1、JOSN Schema描述
formily 是阿里推出的一套动态化表单的解决方案,用于解决传统模式下写动态表带时代码冗余大、性能低、可维护性差的问题,formily 表单采用标准的 JSON Schema 进性描述,可简单的理解为规范化的 JSON 用于描述 form 表单,比如,下面几个字段规范的定义:
type: 字段的数据类型,可以是简单或者复杂数据类型;
properties:对象属性,通俗用于对象嵌套描述;
x-rules: 字段校验属性,Array类型,支持通用的必填、正则校验、函数校验以及错误信息提示;
x-component:字段组件属性,可注入对于的表单组件,相当于FormItem,比如Input、Select等,也可以是CustomComponent,通过渲染层注入组件即可;
x-component-props:用于x-component中指定的组件的属性,相当于FormItem的属性。
(因文章篇幅有限,这里仅列举部分伪代码,详细代码见文章底部链接)
一个简单的 Formily 表单可写成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <SchemaForm labelCol={24} wrapperCol={24} components={{ Input, Select, CheckboxGroup: Checkbox.Group, RadioGroup: Radio.Group, RangePicker: DatePicker.RangePicker, Upload }} schema={simpleSchema} onSubmit={(values) => { console.log(values); }} > <FormButtonGroup offset={0}> <Submit>查询</Submit> <Reset>重置</Reset> </FormButtonGroup> </SchemaForm>
|
Schema 文件描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| export const simpleSchema = { type: "object", properties: { input: { type: "string", title: "输入", required: true, "x-component": "Input", "x-component-props": { placeholder: "请输入" } }, select: { type: "number", title: "下拉选", required: true, "x-component-props": { placeholder: "请选择" }, enum: [ { label: "选项一", value: 1 }, { label: "选项二", value: 2 } ], "x-component": "select" }, radio: { title: "单选", "x-component": "RadioGroup", enum: ["1", "2", "3", "4"] }, checkbox: { title: "复选", "x-component": "CheckboxGroup", enum: [ { label: "One", value: 1 }, { label: "Two", value: 2 }, { label: "Three", value: 3 } ] }, dateRange: { type: "object", title: "时间范围", required: true, properties: { "[start,end]": { "x-component": "RangePicker", "x-component-props": { placeholder: ["开始时间", "结束时间"] } } } }, upload: { type: "array", title: "图片", "x-component-props": { listType: "picture-card", action: "https://www.mocky.io/v2/5cc8019d300000980a055e76" }, "x-component": "upload", description: "仅支持图片类数据上传" } } };
|
2、表单的生命周期/状态
在formily中,一切的联动操作都源自生命周期函数,可分为表单的生命周期函数和表单字段的操作,在表单的 effects 中实现对于的逻辑操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <SchemaForm labelCol={24} wrapperCol={24} components={{ Input, Select, }} schema={basicSchema} actions={basicAction} effects={($, { setFieldState }) => { $("onFieldValueChange", "classType").subscribe((parentState) => { setFieldState("currentToggle", (state) => { state.visible = parentState.value; }); setFieldState("currentStatus", (state) => { state.value = parentState.value ? "显示" : "隐藏"; }); }); }} onSubmit={(values) => { console.log(values); }} > <FormButtonGroup offset={0}> <Submit>查询</Submit> <Reset>重置</Reset> </FormButtonGroup> </SchemaForm>
|
Schema 文件描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| export const basicSchema = { type: "object", properties: { classType: { type: "number", enum: [ { label: "显示", value: 1 }, { label: "隐藏", value: 0 } ], title: "联动①", required: true, default: 1, "x-component": "select", description: "利用生命周期做联动" }, currentToggle: { type: "string", title: "联动①组件", required: true, "x-component": "Input" }, currentStatus: { type: "string", title: "联动①状态", required: true, "x-component": "Input" }, } };
|
如上述案例所示,我们在 SchemaForm 的 effects 中通过订阅生命周期函数来监听字段状态的变化,从而达到表单联动的效果;以上是一种写法,触发生命周期还有另一种写法,通过解构出 FormEffectHooks 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { FormEffectHooks, createFormActions } from '@formily/next' const { onFieldValueChange$, onFormInit$ } = FormEffectHooks const { setFieldState, getFieldState } = createFormActions()
onFormInit$().subscribe(() => { setFieldValue('aa', 123) })
onFieldValueChange$('bb').subscribe( fieldState => { setFieldState('cc', state => { state.visible = fieldState.value === 123 }) })
|
表单的生命周期函数有很多种,详见官方文档,一些常用的生命周期函数如下:
| 常量名 |
常量值 |
描述 |
Hook |
返回值 |
| ON_FORM_SUBMIT |
onFormSubmit |
表单提交时触发 |
onFormSubmit$ |
FormState |
| ON_FORM_RESET |
onFormReset |
表单重置时触发 |
onFormReset$ |
FormState |
| ON_FIELD_CHANGE |
onFieldChange |
字段状态发生变化时触发 |
onFieldChange$ |
FieldState |
| ON_FIELD_INPUT_CHANGE |
onFieldInputChange |
字段输入事件触发时触发 |
onFieldInputChange$ |
FieldState |
| ON_FIELD_VALUE_CHANGE |
onFieldValueChange |
字段值变化时触发 |
onFieldValueChange$ |
FieldState |
3、表单操作actions/effects
1 2 3 4 5 6 7 8 9 10 11 12 13
|
$("onFieldValueChange", "classType").subscribe((parentState) => { setFieldState("currentToggle", (state) => { state.visible = parentState.value; }); });
onFieldValueChange$('bb').subscribe( fieldState => { setFieldState('cc', state => { state.visible = fieldState.value === 123 }) })
|
上面只演示了两段伪代码片段,详情功能可参考上一小节表单的生命周期的代码,我们只需要知道:所有的联动操作都需要在effects实现,而操作Form API都通过actions来控制,详细接口参考文档链接。
4、表单的路径系统
表单的路径系统相当于CSS中的选择器,可以通过路径系统来匹配需要操作的字段;这里的匹配方式大概可分为两种,一种是通配符匹配,另一种是target目标匹配。
4.1 通配符匹配
1 2 3 4 5 6 7 8 9 10
| onFieldValueChange$('array.*.aa').subscribe((parentState) => { })
onFieldValueChange$('aa').subscribe(({ name, value }) => { setFieldState('*(bb,cc,dd)', state => { state.visible = value }) })
|
4.2 target目标匹配
target 相邻查找
- prevPath.[].fieldName代表当前行字段
- prevPath.[+].fieldName代表下一行字段
- prevPath.[-].fieldName代表上一行字段
- prevPath.[+2].fieldName代表下下一行字段
- prevPath.[-2].fieldName代表上上一行字段
一次可以继续往下递增或者递减
target 向前路径查找
- .path.a.b代表基于当前字段路径往后计算
- …path.a.b代表往前计算相对路径
- …path.a.b代表继续往前计算相对路径
以此类推
5、x-linkages属性简单联动
上面说到,一切的联动都源自生命周期,而 x-linkages 用于在协议层描述简单联动,注意,这个只是简单联动,它无法描述异步联动,也无法描述联动过程中的各种复杂数据处理,以下是一个简单的联动案例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <SchemaForm labelCol={24} wrapperCol={24} components={components} schema={basicSchema} actions={basicAction} onSubmit={(values) => { console.log(values); }} > <FormButtonGroup offset={0}> <Submit>查询</Submit> <Reset>重置</Reset> </FormButtonGroup> </SchemaForm>
|
Schema描述文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| export const basicSchema = { type: "object", properties: { linkTwo: { type: "number", enum: [ { label: "联动②显示", value: 1 }, { label: "联动②隐藏", value: 0 }, { label: "联动②控制schema的title字段", value: 3 } ], title: "联动②", required: true, default: 1, "x-component": "select", "x-linkages": [ { type: "value:visible", target: "linkTwoEle", condition: "{{ $value === 1 || $value === 3 }}" }, { type: "value:schema", target: "linkTwoEle", condition: "{{ $value === 3 }}", schema: { title: "这是联动的标题" } } ], description: "利用 x-linkages 属性做联动,这个只是简单联动,它无法描述异步联动,也无法描述联动过程中的各种复杂数据处理。" }, linkTwoEle: { type: "object", title: "联动②组件", required: true, properties: { "[start,end]": { "x-component": "RangePicker", "x-component-props": { placeholder: ["开始时间", "结束时间"] } } } }, } };
|
这里 link 联动的 Type 类型主要有三种:
- value:state,由值变化控制指定字段的状态
- value:visible,由值变化控制指定字段显示隐藏
- 相当于 value:state 的一种特例情况,即 state.visible
- value:schema,由值变化控制指定字段的 schema. 相当于 value:state 的一种特例情况,即 state.props
condition 为link联动触发的条件,target 为上小节描述的target目标匹配。
6、表单的扩展机制(自定义生命周期、自定义扩展状态、自定义校验规则、自定义组件)
6.1 自定义生命周期: 自定义事件派发
自定义事件大概可分为两种:一是通过 createFormActions 全局派发事件,二是在 effects 逻辑联动中通过 notify 来派发事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const extendAction = createFormActions();
extendAction.dispatch("customEvent1", { value: 666, text: "全局的payload" });
const myFormily = () => ( <SchemaForm labelCol={24} wrapperCol={24} components={components} schema={extendSchema} actions={extendAction} effects={($, { notify }) => { // effect派发自定义事件 $("onFieldValueChange", "a2").subscribe((parentState) => { notify("customEvent2", parentState); }); }} onSubmit={(values) => { console.log(values); }} > <FormButtonGroup offset={0}> <Submit>查询</Submit> <Reset>重置</Reset> </FormButtonGroup> </SchemaForm> );
export default myFormily;
|
派发事件之后,需要在自定义组件内通过 useFormEffects 监听自定义事件
1 2 3 4 5 6 7 8 9
| useFormEffects(($, { notify, setFieldState, getFieldState }) => { $("customEvent1").subscribe((payload) => { console.log(payload); }); $("customEvent2").subscribe((payload) => { console.log(payload); }); });
|
6.2 自定义扩展状态:自定义formily组件状态
表单的自定义状态在自定义组件中使用的比较多,类似于 react Hook 的形式,仅有两种形式:
1 2 3 4 5 6 7 8 9 10 11 12
| import { useFieldState, useFormState } from '@formily/next';
const [state, setFieldState] = useFieldState({ extendState1: 'something', extendState2: 'something' })
const [formState, setFormState] = useFormState({ extendState1: 'something', extendState2: 'something' })
|
自定义状态字段和系统提供的状态字段一致,自定义状态改变会也触发 onFieldChange 或 onFormChange 事件。
6.3 自定义组件: 自定义字段组件和虚拟布局组件
Formily 可以通过自定义组件来满足更加复杂的业务需求,通过给组件添加 isFieldComponent 属性即可,一个简单的字段组件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React from "react"; import { useFormEffects, useFieldState } from "@formily/antd";
const CustomFieldComponent = (props) => { const { value, schema, className, editable, path, mutators, form } = props;
const componentProps = schema.getExtendsComponentProps() || {};
return ( <div> <h3 style={{ fontSize: 14 }}>这是自定义字段组件描述</h3> <input value={value || ""} onChange={(e) => mutators.change(e.target.value)} /> </div> ); };
CustomFieldComponent.isFieldComponent = true;
export default CustomFieldComponent;
|
当然,Formily 也提供 registerVirtualBox 方法自定义虚拟组件,主要用于表单布局方面:
1 2 3 4 5 6 7 8 9
| registerVirtualBox("CustomLayout", ({ children, schema }) => { return ( <div style={{ border: "1px solid red", padding: 10 }}> {children} {schema["x-component-props"]["say"]} </div> ); });
|
组件定义好后,可以通过局部注册、全局注册、全局批量扩展三种方式注入到Formily表单系统中
1 2 3 4 5 6 7 8
| <SchemaForm components={{ CustomComponent, CustomFieldComponent }}/>
registerFormField('CustomComponent2', connect()(CustomComponent))
registerFormFields({ CustomComponent3: connect()(CustomComponent) })
|
6.4 自定义校验:自定义x-rules校验、自定义函数验证
在校验中,Formily也提供两种校验方式,一种是直接在schema中定义 x-rules 校验,另一种是通过自定义校验函数来校验,后一种方式常用于校验函数复用。
1 2 3 4 5 6 7 8 9 10
| import { registerValidationRules } from "@formily/antd";
registerValidationRules({ customRule2: (value) => { return value === "123" ? "不能等于123" : ""; } });
|
Schema描述文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| export const extendSchema = { type: "object", properties: { a1: { type: "number", title: "x-rules校验", required: true, "x-component": "input", "x-rules": { validator: (value) => { return value === "123" ? "不能等于123" : ""; } } }, a3: { type: "string", title: "自定义函数校验", required: true, "x-component": "Input", "x-rules": { customRule2: true } }, } };
|
github案例详见 github仓库
在线演示案例详见 codesandbox案例