SchemaForm - JSON 表单
通过一个配置式的 schema 来生成表单。schema 与 Antd Table 的 columns 非常类似。
基本概念
一个基本的 Schema 表单:
在开始深入之前,让我们先来了解几个重要的概念。
SchemaForm 的底层还是 Antd Form 组件。它通过遍历 schema 数组 来生成表单项。
以这个例子来说,columns 数组 中的每一个对象都是一个 schema。
{
title: '用户名',
dataIndex: 'username',
fieldProps: {
Placeholder: '请输入用户名'
},
formItemProps: {
rules: [
{
required: true,
},
],
},
},
如果用 Antd Form 的方式来书写,那么这个 schema 生成的表单项是这样的:
import { Form, Input } from 'antd';
<Form>
<Form.Item
label="用户名"
name="username"
rules={[
{
required: true,
},
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
</Form>;
不难看出,在 schema 中:
title对应的 Form.Item 中的label。dataIndex对应的 Form.Item 中的name。formItemProps的值会传递给 Form.Item 组件。fieldProps的值会传递给被 Form.Item 包裹的组件 (对这个例子来说这个组件是 Input)。
我们再来看一下第二个 schema:
{
title: "性别",
dataIndex: "gender",
valueType: "radio",
fieldProps: {
options: [
{ label: "男", value: "男" },
{ label: "女", value: "女" },
{ label: "其它", value: "其它" },
],
},
};
与第一个 schema 不同的地方在于,它多了一个 valueType 字段,很显然 SchemaForm 通过 valueType 字段渲染出了不同的组件,以这个例子来说 valueType:'radio' 对应的是一个 Radio.Group 组件,并且通过 fieldProps 给它传入了 options 属性。
这是使用 SchemaForm 的优点之一,大部分常用的组件都能通过 valueType 来渲染。
schema 部分介绍完了,我们再来看一下 表单实例 FormInstance。
表单实例 是 Antd Form 中一个比较重要的概念,通过实例中的方法,我们可以完成很多操作,比如:提交表单,重置表单,给表单赋值等。
在 SchemaForm 中,我们通过 formRef 属性来获取 表单实例。
import { useRef } from 'react';
import { ProFormInstance, SchemaForm } from 'react-admin-kit';
const formRef = useRef<ProFormInstance>() // 注意这里使用的类型定义是 ProFormInstance. 它在 antd form instance 的基础上扩展了一些方法
<SchemaForm
formRef={formRef}
>
formRef.current?.submit() // 提交
formRef.current?.resetFields() // 重置
formRef.current?.setFieldsValue() // 赋值
在后文中还会详细介绍 表单实例 中的其它一些方法。
以上就是 SchemaForm 的基本概念,看到这你已经可以开始书写一些简单的表单了 ✨。
随着你使用的不断深入,你可以不断查看下面的各种例子来了解 SchemaForm 的其它属性,相信你会越来越得心应手的。
valueType
valueType 是 schema 里的一个非常重要的字段,通过指定 valueType 就能映射出不同的表单项。常用的 valueType 有 money digit date dateRange select radio textarea 等,完整的列表见这里。当 valueType 没有指定时,默认渲染的表单项是 Input 组件。
如果 valueType 不能满足你的需求,可以使用 renderFormItem 完全自定义表单项。
自定义的组件需要满足 Form.Item 的规范,即组件能接受 value 和 onChange 属性。
{
renderFormItem: (schema, config, form) => {
return <MyComp />;
};
}
required
由于表单中设置必选的频率比较高,schema 中新增了 required 字段,作为 formItemProps: { rules: [{ required: true }]} 的简写。当然 formItemProps 的优先级更高。
表单布局
传统布局
grid 栅格模式
grid 栅格模式是把 FormItem 放入 Ant Design 的 栅格系统 中,通过传入 Row 和 Col 的属性来控制每个 FormItem 的布局。
<SchemaForm
grid
rowProps={{}}
colProps={{}}
columns=[
{
title: 'name',
colProps: {} // 更高优先级
},
{
title: 'age',
colProps: {} // 更高优先级
}
]
>
以这个组件为例,当它开启 grid 属性后,它实际上会被渲染成:
// 伪代码, 仅用于示意.
<Form>
<Row {...rowProps}>
<Col {...colProps}>
<Form.Item>
<Field />
<Form.Item>
</Col>
<Col {...colProps}>
<Form.Item>
<Field />
<Form.Item>
</Col>
</Row>
</Form>
通过栅格系统,表单项的布局将会变得更加的灵活,比如:
grid 水平方向
当开启 grid 并且 layout 为 horizontal 时,表单项的 label 宽度很可能会不一致。如下所示:
有许多方法能让 label 宽度统一。比如给它设定一个最小宽度:
或者给每个表单的 label 单独指定宽度。
labelCol是 Antd Form 的一个属性。同 Col 组件,可以传入{span: 8}或者{flex: 0 0 30%}等属性。
labelCol是以其 所在表单项 的宽度作为基值(24 份)来进行后续计算的。{span: 8}代表 label 占 所在表单项 的宽度的 8 份。{flex: 0 0 30%}代表 label 占 所在表单项 的宽度的 30%。
空白占位
需要强制换行时可用空白占位.
表单操作
只读模式
通过 readonly 属性或者 schema 上的 readonly 属性可以设置只读模式. 只读模式有以下几个注意点:
- 只读模式下的自定义显示用
render方法, 而不是renderFormItem. - 空白占位可以用
render方法返回 null 来实现.render: () => null.
💡 Rakjs 扩展了只读模式下 render 函数的第二个参数 record. 这个 record 里包含了表单里的所有值, 提高了实用性.
只读表格模式
只读模式还能转化成表格的形式,类似 Antd Descriptions 组件。
表单项联动
初始值和表单项赋值
- [是否显示用户] 初始值为否。
- 点击赋值按钮后显示为是。
- 点击重置按钮后显示为否。
提交按钮 submitter
submitter 属性默认为 false. 开启后可自动生成提交按钮.
还可以给生成的按钮传递属性.
只读与编辑切换
下面模拟演示两个真实场景:
1. 模拟一个表单页的回显
2. 只读表单项的依赖显示
表单数据
表单取值
SchemaForm 提供了多种方法来获取表单值. 最常用的是通过 onFinish 属性来取值.
除了 onFinish 以外还可以通过 表单实例 提供的方法来获取表单值.
下面列举了几个常用的取值方法:
| 表单实例方法 | 描述 | 来源 |
|---|---|---|
getFieldsValue | 获取表单值 | Antd Form |
validateFields | 表单验证通过后返回表单值 | Antd Form |
getFieldsFormatValue | 获取表单值(会额外根据 valueType 处理时间格式; 会额外处理 schema 中的 transform 转化) | ProComponent |
onFinish | 同上 | ProComponent |
validateFieldsReturnFormatValue | 表单验证通过后返回表单值(同上额外处理转换) | ProComponent |
这几个方法在取值上有细微的差异,主要的差异点在时间格式的处理,transform 转化等场景下。
简单来说,如果你明确知道需要 转换前 的值,请使用前两种方法,否则请使用后三种方法。
| 表单实例方法 | 结果 |
|---|---|
getFieldsValue | {
"tags": [
"tag01",
"tag02"
],
"validDate": [
"dayjs对象",
"dayjs对象"
]
} |
validateFields | "同上" |
getFieldsFormatValue | {
"tagIds": "tag01,tag02",
"validDate": [
"2024-10-01",
"2024-10-07"
]
} |
onFinish | "同上" |
validateFieldsReturnFormatValue | "同上" |
ConvertValue 和 Transform
有的时候后端返回的数据并不能直接用于表单控件, 需要先对数据进行处理. (ConvertValue)
还有的时候, 在提交表单时, 表单收集到的数据并不能直接提交给后台, 需要先对数据进行处理. (Transform)
很典型的场景就是附件上传.
// 假设有一个附件上传组件
{
title: '附件列表',
dataIndex: 'fileList',
renderFormItem: () => <FormUpload />
}
// 该组件需要接收的是一个对象数组, 对象的字段是 name 和 url.
[
{ name: '文件A', url: 'www.xx.com/xx' },
{ name: '文件B', url: 'www.xx.com/xx' },
];
// 而后端返回的数据是 fileName 和 filePath:
[
{ id: 1, fileName: '文件A', filePath: 'www.xx.com/xx' },
{ id: 2, fileName: '文件B', filePath: 'www.xx.com/xx' },
];
// 提交给后端的数据需要是 fileIds: '1,2'
Schema 中的 convertValue 和 tranform 字段就可以应对这个场景.
附件上传场景可以使用衍生组件中的 FormUpload 组件.
约定式
对于 Select, TreeSelect 等组件, 当其开启了 labelInValue 属性时,组件不再接受字符串而是需要接收一个对象 { label: string, value: string} 来回显。
因此前端需要转化这个数据,而在提交时又要把这个对象拆成原来的字段。
// 比如一个用户下拉组件开启了 labelInValue
{
title: '用户',
dataIndex: 'user',
valueType: 'select',
fieldProps: {
labelInValue: true,
options: [
{ value: 1, label: 'jack' },
{ value: 2, label: 'tom' },
]
}
}
// 后端返回 userId: 1, userName: 'jack' 用于回显
// => 前端需转化成 user: { value: 1, label: 'jack'}
// => 提交时需要把对象再转化成 userId: 2, userName: 'tom'
由于这类场景比较常见的,RAK 想通过对 dataIndex 的约定来简化这一流程,约定如下:
- 👉 如果
dataIndex中包含逗号,, RAK 会根据逗号前后的字段来自动拼接成一个对象,提交时又会把该对象拆分。逗号前的字段映射成 value,逗号后的字段映射成 label。
// 比如一个用户下拉组件开启了 labelInValue
{
title: '用户',
dataIndex: 'userId,userName',
valueType: 'select',
fieldProps: {
labelInValue: true,
options: [
{ value: 1, label: 'jack' },
{ value: 2, label: 'tom' },
]
}
}
// 由于符合约定式只需要这样给值就可以
initialValues={{ userId: 1, userName: 'jack' }}
// 或者
formRef.current?.setFieldsValue({ userId: 1, userName: 'jack' })
// 提交时表单不会拿到:
`{'userId,userName': { value: 1, label: 'jack' }}`
// 而是会拿到:
{
userId: 1,
userName: 'jack'
}
- 如果组件接受对象的键值不是 value 和 label,还可以通过下划线自定义。比如
userId,userName_id,name,RAK 会拆分下划线,下划线后面的字段同样用逗号隔开。以这个例子来说,当给组件赋值{userId: 1, userName: 'jack'}时, 值会被转换成{ id: 1, name: 'jack' }传给组件。
利用 innerRef 存储额外信息
innerRef是 RAK 提供的一个工具类 ref, 里面包含了一些实用的方法可以用来简化一些特殊的场景.
在 fieldProps 的第二个参数里默认提供了 innerRef, 可以用 innerRef.current?.setData()来存储额外的信息, 然后在其它的表单项里消费 innerRef.
innerRef 中的 setData 和 react 的 setState 一样, 只需要传入关心的字段就可以, 不会覆盖其它的字段.
对于内嵌模式 (embed), innerRef 可以传到 ProForm 上.
import { ProForm } from 'react-admin-kit';
import { useRef } from 'react'
const innerRef = useRef();
...
<ProForm
innerRef={innerRef}
...
>
...
</>
高级布局
内嵌模式 (Embed)
对于复杂表单, 通过内嵌模式可以让每个区块单独设置布局, 同时通过设置 valueBaseName 属性, 数据也可以收集在各自的对象里.
import { ProForm, SchemaForm } from 'react-admin-kit';
import { Card } from 'antd';
<ProForm>
<SchemaForm embed valueBaseName="one" />
<div>
<SchemaForm embed valueBaseName="two" />
</div>
<Card>
<SchemaForm embed valueBaseName="three" />
</Card>
</ProForm>;
// 表单提交时收集到的值为 { one: ..., two: ..., three: ... }`
👉 需要注意的是, 在 embed 模式下, valueBaseName 的实现仅仅只是把 schema 中的 dataIndex 转换成数组. 见 Antd 的这个例子.
所以在 setFieldsValue 的时候, 需要把 valueBaseName 的值也考虑进去.
setFieldsValue({ business: { company: 'xxx' } });同时在做联动控制时, 当 valueType='dependency'并且 valueBaseName 有值时,
name里的值应该是套嵌数组.{ valueType: 'dependency', name: [['business', 'serviceName']] } 👈
分组布局 (Group)
当 valueType 为 group 时即开启分组布局。每个 group 相当于是一个区块,columns里的内容会生成表单项。
默认情况下这些表单项是以 Space 组件包裹的。所以你可以在 fieldProps 里传入 Space 的 api
分组布局 (Grid)
group 的布局分为两层,外层(区块标题)和内层(columns 里的表单项),所以 colProps 的设置也要分两层设置。
表单数组 FormList
当 valueType 为 formList 时能够生成表单数组, 这对于收集 数组 信息非常有用.
比如下面的例子能够添加多个店铺.
formList 实际上是一个组件, api 见这里, 可以通过 fieldProps 给这个组件传递属性.
Grid 排列
要让表单数组内的表单项 grid 排列, 除了开启 grid 属性以外, 表单项还需要用 valueType='group' 包裹.
样式自定义
还可以通过 itemRender 属性自定义样式和操作按钮。用法见示例。
itemRender 的参数类型是
({ listDom, action }, options) => ReactNodeoptions:
{name, field, index, record, fields, operations, meta}
API
SchemaForm
SchemaForm 类型是 SchemaFormSelfType 和 SchemaFormOriginType 的结合。
SchemaFormSelfType
| 属性名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| embed | 是否为内嵌模式 | boolean | false |
| valueBaseName | 开启embed后处理套嵌数据结构; 在onFinish收集数据时, 会挂在该字段下. 仅适用于embed模式 | string | false |
| readonly | 是否为只读模式 | boolean | false |
| readonlyType | 只读时的展示形式。可选 form 或 descriptions | "form" | "descriptions" | form |
| columns | 表单项的配置描述; | (必选) | |
| onFinish | 表单提交时的回调; | (values) => Promise | void | -- |
| formRef | 用于获取form实例; 请使用formRef而不要通过form属性传入一个form实例来获取实例. 因为RAK组件对form实例进行了额外的封装, 一定要通过formRef来获取. | RefObject<ProFormInstance> | -- |
| innerRef | RAK特有的ref, 用于存放一些工具类函数和数据. | -- | |
| submitter | 提交按钮相关配置. | boolean | SubmitterProps & { style: React.CSSProperties } | false |
| descriptionsProps | 描述模式下的表格样式配置 | Omit<DescriptionsProps, 'items' | 'columns'> | -- |
SchemaFormOriginType
| 属性名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| form | -- | FormInstance<Record<string, any>> | -- |
| name | -- | string | -- |
| initialValues | -- | Store | -- |
| component | -- | string | false | FC<any> | ComponentClass<any, any> | -- |
| fields | -- | FieldData<any>[] | -- |
| validateMessages | -- | ValidateMessages | -- |
| onValuesChange | -- | ((changedValues: any, values: Record<string, any>) => void) | -- |
| onFieldsChange | -- | ((changedFields: FieldData<any>[], allFields: FieldData<any>[]) => void) | -- |
| onFinishFailed | -- | ((errorInfo: ValidateErrorEntity<Record<string, any>>) => void) | -- |
| validateTrigger | -- | string | false | string[] | -- |
| preserve | -- | boolean | -- |
| clearOnDestroy | -- | boolean | -- |
| prefixCls | -- | string | -- |
| colon | -- | boolean | -- |
| layout | -- | FormLayout | -- |
| labelAlign | -- | FormLabelAlign | -- |
| labelWrap | -- | boolean | -- |
| labelCol | -- | ColProps | -- |
| wrapperCol | -- | ColProps | -- |
| feedbackIcons | -- | FeedbackIcons | -- |
| size | -- | SizeType | -- |
| disabled | -- | boolean | -- |
| scrollToFirstError | -- | boolean | ScrollFocusOptions | -- |
| requiredMark | -- | RequiredMark | -- |
| hideRequiredMark | Will warning in future branch. Pls use `requiredMark` instead. | boolean | -- |
| rootClassName | -- | string | -- |
| variant | -- | "outlined" | "borderless" | "filled" | "underlined" | -- |
| loading | 表单按钮的 loading 状态 | boolean | -- |
| onLoadingChange | 这是一个可选的属性(onLoadingChange),它接受一个名为loading的参数,类型为boolean,表示加载状态是否改变。 | ((loading: boolean) => void) | -- |
| formRef | 获取 ProFormInstance | MutableRefObject<any> | RefObject<any> | -- |
| syncToUrl | 同步结果到 url 中 | boolean | ((values: Record<string, any>, type: "get" | "set") => Record<string, any>) | -- |
| syncToUrlAsImportant | 当 syncToUrl 为 true,在页面回显示时,以url上的参数为主,默认为false | boolean | -- |
| extraUrlParams | 额外的 url 参数 中 | Record<string, any> | -- |
| syncToInitialValues | 同步结果到 initialValues,默认为true如果为false,reset的时将会忽略从url上获取的数据 | boolean | -- |
| omitNil | 如果为 false,会原样保存。 | boolean | true |
| dateFormatter | 格式化 Date 的方式,默认转化为 string | false | "string" | "number" | (string & {}) | ((value: Dayjs, valueType: string) => string | number) | -- |
| onInit | 表单初始化成功,比如布局,label等计算完成 | ((values: Record<string, any>, form: ProFormInstance<any>) => void) | -- |
| params | 发起网络请求的参数 | Record<string, any> | -- |
| request | 发起网络请求的参数,返回值会覆盖给 initialValues | ProRequestData<Record<string, any>, Record<string, any>> | -- |
| isKeyPressSubmit | 是否回车提交 | boolean | -- |
| formKey | 用于控制form 是否相同的key,高阶用法 | string | -- |
| autoFocusFirstInput | -- | boolean | -- |
| readonly | 是否只读模式,对所有表单项生效 | boolean | -- |
| grid | open grid layout | boolean | false |
| colProps | only works when grid is enabled | ColProps | { xs: 24 } |
| rowProps | only works when grid is enabled | RowProps | { gutter: 8 } |
SchemaFormInnerRefType
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| data | 可以存储表单中的额外数据 | Record<string, any> | {} |
| setData | 存入数据; setData 和 react 的 setState 一样, 只需要传入关心的字段就可以, 不会覆盖其它的字段. | (Record<string, any>) => void | -- |
FormColumnType
| 属性名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| fieldProps | 给 fieldProps 方法注入 innerRef | object | ((form: ProFormInstance, innerRef: BaseInnerRef, config: any) => object) | -- |
| renderFormItem | 给 renderFormItem 方法注入 innerRef | ((item: any, config: any, form: any, innerRef: BaseInnerRef) => any) | -- |
| columns | 重新定义 columns 类型 | FormColumnType<any, "text">[] | ((values: any) => FormColumnType<any, "text">[]) | -- |
| required | 是否必选; formItemProps: { rules: [{ required: true }] } 的简写 | boolean | -- |
| dataIndex | 可使用约定式自动处理值: userId, userName 或 userId, userName_id, name; | string | -- |
| name | -- | any | -- |
| className | -- | string | -- |
| title | 支持 ReactNode 和 方法 | ReactNode | ((schema: ProSchema<any, { tooltip?: ReactNode; key?: Key; className?: string | undefined; width?: string | number | undefined; name?: any; defaultKeyWords?: string | undefined; } & Pick<...> & { ...; }, ProSchemaComponentTypes, FormFieldType | "text", unknown>, type: ProSchemaComponentTypes,... | -- |
| params | 从服务器请求的参数,改变了会触发 reload | Record<string, any> | ((record: any, column: ProSchema<any, { tooltip?: ReactNode; key?: Key; className?: string | undefined; width?: string | number | undefined; name?: any; defaultKeyWords?: string | undefined; } & Pick<...> & { ...; }, "form", "text", unknown>) => Record<...>) | undefined | -- |
| request | 从服务器请求枚举 | ProFieldRequestData<any> | -- |
| readonly | 是否只读模式 | boolean | -- |
| colProps | only works when grid is enabled | ColProps | { xs: 24 } |
| rowProps | only works when grid is enabled | RowProps | { gutter: 8 } |
| key | 确定这个列的唯一值,一般用于 dataIndex 重复的情况 | Key | -- |
| tip | -- | string | -- |
| tooltip | 展示一个 icon,hover 是展示一些提示信息 | ((string | number | boolean | (TooltipPropsWithTitle & { icon?: ReactElement<any, string | JSXElementConstructor<any>>; }) | (TooltipPropsWithOverlay & { ...; }) | ReactElement<...> | Iterable<...> | ReactPortal) & (string | ... 4 more ... | ReactPortal)) | null | undefined | -- |
| valueEnum | 支持 object 和Map,Map 是支持其他基础类型作为 key | ProSchemaValueEnumObj | ProSchemaValueEnumMap | ((row: any) => ProSchemaValueEnumObj | ProSchemaValueEnumMap) | -- |
| formItemProps | 自定义的 formItemProps | FormItemProps<any> | ((form: FormInstance<any>, config: { key?: Key; dataIndex?: unknown; title?: ReactNode | ((schema: ProSchema<any, { tooltip?: ReactNode; ... 4 more ...; defaultKeyWords?: string | undefined; } & Pick<...> & { ...; }, ProSchemaComponentTypes, FormFieldType | "text", unknown>, type: Pr... | -- |
| renderText | 修改的数据是会被 valueType 消费 | ((text: any, record: any, index: number, action: ProCoreActionType<{}>) => any) | -- |
| render | Render 方法只管理的只读模式,编辑模式需要使用 renderFormItem | ((dom: ReactNode, entity: any, index: number, action: ProCoreActionType<{}>, schema: { key?: Key | undefined; dataIndex?: unknown; title?: ReactNode | ((schema: ProSchema<...>, type: ProSchemaComponentTypes, dom: ReactNode) => ReactNode); ... 17 more ...; proFieldProps?: (ProFieldProps & Record<...>) | u... | -- |
| debounceTime | request防抖动时间 默认10 单位ms | number | -- |
| dependencies | 依赖字段的name,暂时只在拥有 request 的项目中生效,会自动注入到 params 中 | any[] | -- |
| ignoreFormItem | 忽略 FormItem,必须要和 renderFormItem 组件一起使用 | boolean | -- |
| hideInForm | 在 Form 中隐藏 | boolean | -- |
| width | -- | string | number | -- |
| defaultKeyWords | -- | string | -- |
| index | -- | number | -- |
| colSize | 每个表单占据的格子大小 | number | -- |
| initialValue | 搜索表单的默认值 | any | -- |
| convertValue | 获取时转化值,一般用于将数据格式化为组件接收的格式 | SearchConvertKeyFn | -- |
| transform | 提交时转化值,一般用于将值转化为提交的数据 | SearchTransformKeyFn | -- |
| order | Form 的排序 | number | -- |
| valueType | -- | "color" | "group" | "formList" | "formSet" | "divider" | "dependency" | "index" | "text" | "checkbox" | "option" | "radio" | "slider" | "switch" | "date" | "time" | "password" | ... 36 more ... | -- |