form-render / mobile 创建自定义嵌套 / 表格组件
x-render 系列里的 form-render / form-render-mobile 是表单自动生成领域常用的工具,并且也内置了多种类型的嵌套组件和列表组件。
但是官方文档里对于如何自定义嵌套、表格组件却没有介绍,网上也搜不到类似的文章。所以这篇文章就来详细介绍下如何在 form-render 以及 form-render-mobile(下文简称 fr-mobile)中自定义嵌套和列表组件,同时也会提到 form-render 1.x 和 2.x 的实现起来的区别以及对应的源码指路。
文末会给出完整的 demo 项目仓库地址,需要的可以自取。
form-render 自定义嵌套组件
先看一个基础的嵌套组件 schema(来自官方 demo):
const schema = {
type: "object",
displayType: "row",
properties: {
obj: {
type: "object",
title: "卡片主题",
description: "这是一个对象类型",
widget: "collapse",
column: 3,
properties: {
input1: {
title: "输入框 A",
type: "string"
},
input2: {
title: "输入框 B",
type: "string"
},
}
}
}
};
其中的 schema.properties.obj
就是一个嵌套组件,form-render 会根据它是否包含 properties 属性来决定要不要当作一个嵌套组件来渲染。而其中的 collapse 则是 form-render 内置的一个收起组件。
如果你使用的是 form-render 2.x,那自定义起来就会简单不少,创建组件,进行自定义操作后渲染 props.children,然后把这个自定义组件提供给 FormRender 的 widgets 即可:
const MyCard = (props) => {
return (
<div style={{ padding: 16, margin: 16, backgroundColor: '#eee' }}>
{props.children}
</div>
);
};
function App() {
const form = useForm();
return (
<FormRender
schema={schema}
form={form}
widgets={{ MyCard }}
/>
);
}
当 form-render 根据 schema 把你这个组件识别成一个嵌套组件时,会自动的帮你把内部的表单项渲染好,然后塞到 props.children 里,所以我们的自定义组件直接渲染即可。
然后把 schema 里的 widget 改成对应的自定义组件名字(注意大小写)就行了,效果如下:
但是很多人会遇到一个问题就是,嵌套组件内部的表单项会被包裹进一个新对象(这里是 obj)里,而不是直接平铺在 form 数据的根对象里:
严格意义上来说,这并不是一个问题,因为 schema 里的描述语义就是这样的,你嵌套对象里的值,就应该存放在一个嵌套的对象里,平铺在根对象里反而不符合语义。
但是有时候就是会有这样的需求:我的嵌套组件只是对样式进行了修改,我希望数据结构并不会产生嵌套。因为嵌套就会造成数据处理起来会从简单的循环变成递归。
这种情况也是可以实现的,首先我们需要知道,form-render 里没什么黑魔法,数据结构的嵌套本质上还是 Form.Item 的路径嵌套实现的。所以可以自己去实现 Form.Item 来处理对应的数据层级:
const MyCard2: React.FC<any> = (props) => {
return (
<Form.Item label="内部组件" name="a123">
<Input />
</Form.Item>
);
};
这时候就能发现,嵌套的层级已经消失了,数据如同我们期望的那样保存在根对象下:
但是总不能我们自己再把每个表单项的 schema 处理手动实现一遍吧?确实不用,form-render 中有一个内部函数 RenderCore
,顾名思义,这个函数包含了最核心的表单项渲染功能,可以通过这个路径引入到咱们的组件里来:
import RenderCore from "form-render/es/render-core";
注意!是 render-core,而不是 form-core,别写错了。
RenderCore 应该怎么用?
这个函数的用法很简单,你可以直接 ctrl 点一下看看它的类型:
interface RenderCoreProps {
schema: any;
rootPath?: any[] | undefined;
parentPath?: any[] | undefined;
[key: string]: any;
}
declare const RenderCore: (props: RenderCoreProps) => any;
我们用到的就是前三个参数,schema 是要渲染的表单项配置,rootPath 代表当前数据的存放根路径,parentPath 代表当前数据的父节点路径。
后两者的含义可能比较模糊啊,不过我们这里不用太深入,只需要知道这两者决定了你数据的存放路径,所以我们两个都传 []
就可以把数据保存在根对象里了:
const MyCard3 = (props) => {
return RenderCore({
schema: props.schema,
rootPath: [],
parentPath: []
});
};
很简单对吧,效果也和我们预期的一样,数据被保存到根对象之下:
不过这里有个需要注意的地方,比如我上面的 schema 里指定了 column: 3
,正常情况下应该把表单项渲染成三列,但是用 RenderCore 渲染出来的还是默认的单列样式。原因在于,RenderCore 的作用是 渲染 schema 内部的表单项,所以你需要手动把嵌套组件的 props 处理成对应的样式。
再进一步,你可能会想:现在是调用一次 RenderCore 就把内部所有表单项都渲染出来了,能不能一次只渲染一个表单项?这样就可以实现灵活更高的控制了,比如对表单项进行排序,或者每个表单项都有特定的包裹样式。
这也是可以的,做法也很简单粗暴,就是遍历一下 properties,把每个表单项都分别放在一个 object 配置项里:
import sortProperties from "form-render/es/models/sortProperties";
const itemSchema = schema?.items?.properties || {};
const fieldSchemas = sortProperties(Object.entries(itemSchema)).map(([dataIndex, item]) => {
const fieldSchema = {
type: 'object',
properties: {
[dataIndex]: item
}
};
});
这么一顿操作之后,其实就相当于我们得到了很多个“嵌套组件”配置项,而每个组件里只包含一个表单项。之后我们给 RenderCore 传入指定的 fieldSchemas[index]
即可实现单独渲染某个表单项,效果我们这里就不赘述了,下面讲列表的时候会用到这个操作。
sortProperties
是 form-render 内置的一个纯函数,用于生成明确的排序,推荐使用。
至此,我们已经介绍了由浅入深的几种自定义嵌套组件的方法。但是无论用哪种方法,都应该保证:在实现需求的情况下尽可能的遵守 schema 的描述。
最后需要明确的是,RenderCore 是为了渲染内部的表单项才使用的,如果在你的场景里并不需要自定义嵌套组件,只是想单纯的修改数据结构的话,那么官方提供的 bind 功能会更适合你,文档链接在这里 数据转换 (xrender.fun)。
form-render 自定义表格组件
然后来看看怎么自定义表格,先来看一个标准表格的 schema:
const schema = {
type: 'object',
displayType: 'row',
properties: {
list: {
title: '活动模版',
type: 'array',
widget: 'TableList',
items: {
type: 'object',
properties: {
input1: {
title: '输入框 A',
type: 'string',
},
input2: {
title: '输入框 B',
type: 'string',
},
input3: {
title: '输入框 C',
type: 'string',
},
},
},
},
},
};
和嵌套组件类似,但是内部的表单项配置变成了一个 items 属性,这里可以介绍下相关 schema 规则:
- 如果 schema 包含 items 属性,就会被认为是列表组件(优先度更高)
- 如果 schema 包含 properties 属性,就会被认为是嵌套组件
同样的,这里的 widget TableList 也是 form-render 内置的一个表格组件,其实 form-render 内置了非常多的表格,链接在这里:内置表格组件 (xrender.fun),有符合需求的就不用自己再实现一遍了。
想要自定义表格组件的流程和嵌套组件一样:创建一个组件,塞给 form-render 的 widgets 里,最后给你的 schema 配置项里指定对应的组件名即可。
我们详细看一下组件内部应该怎么实现,比较长啊,你可以跟着下面的介绍回头一步步对着代码看:
const MyList = (compProps) => {
const {
fields = [],
removeItem,
addItem,
rootPath,
schema,
renderCore,
} = compProps;
const itemSchema = schema?.items?.properties || {};
const { props = {} } = schema;
const { pagination = {}, ...rest } = props;
const paginationConfig = pagination && {
size: 'small',
hideOnSinglePage: true,
...pagination,
};
const columns = sortProperties(Object.entries(itemSchema)).map(([dataIndex, item]) => {
return {
dataIndex,
width: item.width || FIELD_LENGTH,
title: item.title,
render: (_, field) => {
const fieldSchema = {
type: 'object',
properties: {
[dataIndex]: {
...itemSchema[dataIndex],
fieldCol: { span: 24 },
}
}
};
return (
<div className='fr-table-cell-content'>
{
renderCore({
parentPath: [field.name],
rootPath: [...rootPath, field.name],
schema: fieldSchema
})
}
</div>
)
}
};
});
columns.push({
title: '操作',
key: '$action',
fixed: 'right',
align: 'center',
width: 80,
render: (value, record) => {
return (
<div>
{!props.hideDelete && (
<Button type='text' danger onClick={() => removeItem(record.name)}>
删除
</Button>
)}
</div>
);
},
});
return (
<div style={{ width: '100%' }} className="fr-table-list">
<Table
scroll={{ x: 'max-content' }}
columns={columns}
dataSource={fields}
rowKey='index'
size='small'
pagination={paginationConfig}
{...rest}
/>
<Button block style={{ marginTop: 8 }} type='dashed' onClick={() => addItem()}>
新增
</Button>
</div>
);
};
注意看这个组件里边调用 renderCore 就指定了不同 rootPath 和 parentPath。
上面这个组件实现的效果是这样的:
核心思路其实不复杂,就是拿到把每个表单项的配置拆出来(这里就用到了上面嵌套组件里提到的渲染单个表单项),然后生成对应的表格列配置。然后把各种 props 里传递进来的配置组装一顿,塞给 Table 组件就完事了。
其实就是配置项多了点,导致代码写的比较长。
这里需要注意的是,form-render 里自己对列表组件做了处理,会通过 props 里提供诸如新增一项、删除一项、移动排序之类的方法,还帮你自动生成了很多有用的配置项,我们可以打印一个 props 看一下:
看名字就知道是干什么的,这里就不过多介绍了。
另一个需要注意的地方是 props.renderCore
,这个和上面嵌套组件里提到的 RenderCore 是一样的。只不过这里自动帮你注入进来了,更方便一些。
最后要说的是,这个自定义出来的 MyList 组件,其实就是仿照内置的 TableList 组件写的。如果你想实现其他的自定义列表,可以直接从内置列表组件里找一个类似的,然后去源码里抄一下。源码链接:
x-render/packages/form-render/src/widgets at master · alibaba/x-render · GitHub
另外还有一个小坑,你可能实现完之后发现自己的表格长这样:
每个单元格前面都有个 label。并且用配置项里提供的 fieldCol={{ span: 24 }}
和 labelCol={{ span: 0 }}
也没效果。
实际上内置的表格组件也没解决这个问题,只是用样式把 label 隐藏起来了而已。直接给你 Table 组件(或者外层的 div 组件加个 class: fr-table-list 即可)
return (
<div className="fr-table-list"> // 看这里
<Table />
</div>
);
背后的隐藏方式也很简单:
fr-mobile 自定义嵌套组件
OK,讲完了 PC,再讲一下移动端怎么适配自定义组件。
其实基本流程还是一样的,声明组件、传递给 FormRender 的 widgets,然后在 schema 里提供对应的组件名。
区别可能就在于 RenderCore 的引入变了,因为我们是移动端,所以是从 form-render-mobile 取的渲染核心:
import RenderCore from 'form-render-mobile/es/render-core';
这是一个简单的自定义折叠组件:
export const MyCard = (props) => {
return (
<AntdCollapse
defaultActiveKey={['1']}
>
<AntdCollapse.Panel
title={
<div style={{ fontWeight: 700 }}>{props?.schema?.title}</div>
}
key="1"
>
{RenderCore(props)}
</AntdCollapse.Panel>
</AntdCollapse>
);
};
效果如下:
可以看到,这个结构下数据还是直接存在根对象里的,如果你想实现嵌套对象的话,自己设置 RenderCore 的 parentPath 参数即可。
fr-mobile 自定义表格组件
最后是移动端的表格组件,这里其实有个坑:fr-mobile 只会渲染内置的列表组件,不会去我们提供的自定义 widgets 里查对应的组件。这就导致了你就算 schema 里配的 widget 是自己的组件名,渲染时也依旧会用内置的列表组件。
解决办法就是先递归整个 schema,把 schema.type 为 array
的改成 any
。由此把列表降级成普通的表单项来使用我们自定义的表格组件。
相关代码在 render-core/index.tsx,感兴趣的可以自己研究下。
知道了这一点之后,自定义列表组件就没啥难点了,下面就是一个简单的例子,包括如何把列表项内部的表单项双排显示:
const MyList = (props) => {
const { schema = {}, readOnly = false } = props;
return (
<Grid.Item span={24}>
<Form.Array
name={[props.id]}
renderAdd={!readOnly ? () => (
<span>
添加
</span>
) : undefined}
onAdd={({ add }) => add()}
renderHeader={({ index }, { remove }) => (
<div>
{schema.title && (
<span>{schema.title} {index + 1}</span>
)}
{!readOnly && (
<a onClick={() => remove(index)} style={{ float: 'right' }}>
删除
</a>
)}
</div>
)}
>
{fields => fields.map(({ index, key }) => {
return (
<Grid columns={2} key={key}>
{RenderCore({
schema: schema.items,
parentPath: [index],
rootPath: [props.id, index]
})}
</Grid>
);
})}
</Form.Array>
</Grid.Item>
);
};
效果如图:
另外如果你遇到了列表组件之外的表单项被挤到一行里的 bug:
把你 form-render-mobile 的版本升级到 ^1.0.7-beta.1
或者更高就解决了。
form-render 1.x 和 2.x 的区别
如果你项目中是使用的 form-render 1.x 的话,那么有几点需要注意一下。
渲染核心的功能不一致
1.x 里使用的渲染核心叫做 Core
,导入链接为:
import Core from 'form-render/es/form-render-core/src/core'
其接受的参数也不一样:
<Core
hideTitle={true}
displayType="inline"
key={index.toString()}
id={child}
dataIndex={childIndex}
/>
嵌套组件和列表组件的 props 不一样
比如列表组件中会把列表项当作一个字符串数组传递给列表组件的 children 里(数组元素即上面 Core 传参里的 child)。
如果你在用 1.x 的话,我会首先推荐你尝试升级到 2.x,这个升级并没有想象中的麻烦。不过如果你的项目太大没法升级的话,也可以比着源码照抄,这里指个路:
- 把 x-render 项目 clone 下来,切换到
v1.14.0
tag - 列表组件实现(包含如何调用 Core 组件,其他的列表组件也在这里):packages\form-render\src\form-render-core\src\core\RenderChildren\RenderList\TableList.js
- Core 组件实现:packages\form-render\src\form-render-core\src\core\index.js
示例 demo
相关示例代码已经上传到 github,链接:HoPGoldy/x-render-custom-demo (github.com),用法见 readme。
转载自:https://juejin.cn/post/7249584657599610917