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
// jsx
<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
// schema
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]": {
// title: "RangePicker",
"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
// jsx
<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
// schmema
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
// jsx
import { FormEffectHooks, createFormActions } from '@formily/next'
const { onFieldValueChange$, onFormInit$ } = FormEffectHooks
const { setFieldState, getFieldState } = createFormActions()

// 表单初始化完成后,执行将字段aa的值修改为123
onFormInit$().subscribe(() => {
setFieldValue('aa', 123)
})

// 当字段bb的值发生变化后,修改字段cc的显示隐藏状态
onFieldValueChange$('bb').subscribe( fieldState => {
// fieldState为bb的当前状态值
setFieldState('cc', state => {
// state为cc的当前状态值,根据字段bb的值是否为123来决定cc的隐藏属性。
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
// jsx
// 片段一
$("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
// 通配符匹配 匹配array字段下任意字段之后是aa的字段(array -> 任意 -> aa)
onFieldValueChange$('array.*.aa').subscribe((parentState) => {
// ...
})
// 通配符匹配 当aa字段值改变后匹配所有的bb、cc、dd字段
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 }}", //当值为3时发生联动
schema: {
title: "这是联动的标题"
}
}
],
description:
"利用 x-linkages 属性做联动,这个只是简单联动,它无法描述异步联动,也无法描述联动过程中的各种复杂数据处理。"
},
linkTwoEle: {
type: "object",
title: "联动②组件",
required: true,
properties: {
"[start,end]": {
// title: "RangePicker",
"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 监听自定义事件
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';

//为当前组件对应的字段中添加自定义的状态字段extendState1和extendState2.
const [state, setFieldState] = useFieldState({
extendState1: 'something',
extendState2: 'something'
})
//为当前组件所在的表单中添加自定义的状态字段extendState1和extendState2.
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;

//获取”x-component-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
// 注册virtual组件 一般用于布局
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案例