跳到主要内容

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 组件。

option 1

如果 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 的 栅格系统 中,通过传入 RowCol 的属性来控制每个 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 属性可以设置只读模式. 只读模式有以下几个注意点:

  1. 只读模式下的自定义显示用 render 方法, 而不是 renderFormItem.
  2. 空白占位可以用 render 方法返回 null 来实现. render: () => null.

💡 Rakjs 扩展了只读模式下 render 函数的第二个参数 record. 这个 record 里包含了表单里的所有值, 提高了实用性.

a1with ID: 1
-
-
-
-

只读表格模式

只读模式还能转化成表格的形式,类似 Antd Descriptions 组件。

a1with ID: 1
-
-
-
-

表单项联动

初始值和表单项赋值

  1. [是否显示用户] 初始值为否。
  2. 点击赋值按钮后显示为是。
  3. 点击重置按钮后显示为否。

提交按钮 submitter

submitter 属性默认为 false. 开启后可自动生成提交按钮.

还可以给生成的按钮传递属性.

只读与编辑切换

下面模拟演示两个真实场景:

1. 模拟一个表单页的回显

2. 只读表单项的依赖显示

ID: -, 姓名: -, 爱好: -

表单数据

表单取值

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 中的 convertValuetranform 字段就可以应对这个场景.

附件上传场景可以使用衍生组件中的 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) => ReactNode

options: {name, field, index, record, fields, operations, meta}

1
请选择

API

SchemaForm

SchemaForm 类型是 SchemaFormSelfTypeSchemaFormOriginType 的结合。

SchemaFormSelfType

属性名描述类型默认值
embed是否为内嵌模式booleanfalse
valueBaseName开启embed后处理套嵌数据结构; 在onFinish收集数据时, 会挂在该字段下. 仅适用于embed模式stringfalse
readonly是否为只读模式booleanfalse
readonlyType只读时的展示形式。可选 form 或 descriptions"form" | "descriptions"form
columns表单项的配置描述;(必选)
onFinish表单提交时的回调;(values) => Promise | void--
formRef用于获取form实例; 请使用formRef而不要通过form属性传入一个form实例来获取实例. 因为RAK组件对form实例进行了额外的封装, 一定要通过formRef来获取.RefObject<ProFormInstance>--
innerRefRAK特有的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--
hideRequiredMarkWill 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获取 ProFormInstanceMutableRefObject<any> | RefObject<any>--
syncToUrl同步结果到 url 中boolean | ((values: Record<string, any>, type: "get" | "set") => Record<string, any>)--
syncToUrlAsImportant当 syncToUrl 为 true,在页面回显示时,以url上的参数为主,默认为falseboolean--
extraUrlParams额外的 url 参数 中Record<string, any>--
syncToInitialValues同步结果到 initialValues,默认为true如果为false,reset的时将会忽略从url上获取的数据boolean--
omitNil如果为 false,会原样保存。booleantrue
dateFormatter格式化 Date 的方式,默认转化为 stringfalse | "string" | "number" | (string & {}) | ((value: Dayjs, valueType: string) => string | number)--
onInit表单初始化成功,比如布局,label等计算完成((values: Record<string, any>, form: ProFormInstance<any>) => void)--
params发起网络请求的参数Record<string, any>--
request发起网络请求的参数,返回值会覆盖给 initialValuesProRequestData<Record<string, any>, Record<string, any>>--
isKeyPressSubmit是否回车提交boolean--
formKey用于控制form 是否相同的key,高阶用法string--
autoFocusFirstInput--boolean--
readonly是否只读模式,对所有表单项生效boolean--
gridopen grid layoutbooleanfalse
colPropsonly works when grid is enabledColProps{ xs: 24 }
rowPropsonly works when grid is enabledRowProps{ gutter: 8 }

SchemaFormInnerRefType

参数说明类型默认值
data可以存储表单中的额外数据Record<string, any>{}
setData存入数据; setData 和 react 的 setState 一样, 只需要传入关心的字段就可以, 不会覆盖其它的字段.(Record<string, any>) => void--

FormColumnType

属性名描述类型默认值
fieldProps给 fieldProps 方法注入 innerRefobject | ((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从服务器请求的参数,改变了会触发 reloadRecord<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--
colPropsonly works when grid is enabledColProps{ xs: 24 }
rowPropsonly works when grid is enabledRowProps{ 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 是支持其他基础类型作为 keyProSchemaValueEnumObj | ProSchemaValueEnumMap | ((row: any) => ProSchemaValueEnumObj | ProSchemaValueEnumMap)--
formItemProps自定义的 formItemPropsFormItemProps<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)--
renderRender 方法只管理的只读模式,编辑模式需要使用 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...--
debounceTimerequest防抖动时间 默认10 单位msnumber--
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--
orderForm 的排序number--
valueType--"color" | "group" | "formList" | "formSet" | "divider" | "dependency" | "index" | "text" | "checkbox" | "option" | "radio" | "slider" | "switch" | "date" | "time" | "password" | ... 36 more ...--