最近的工作涉及到了很多表单,也在不知不觉中意识到了自己对表单的一无所知。
今天,就来聊聊Formily。
Formily 只是众多表单处理方式里的一种。
我们必须要先说说表单本身。
最初我对表单的理解可能是这个样子的

但是实际的 Form 表单场景却是
这样的

这样的

等等...
作为一个 ToB 的前端开发,从我的角度去看,Form 表单在 B 端的使用是相当灵活且广泛的。
上图给出的场景,仅仅是表单形式的不同。
Form 场景的复杂还体现在
等等...
通过一个多月对 Formily 从无到有的接触,再加上从组内其他熟悉 Formily 的小伙伴里取来的经。
我总结了几种在使用 Fomily 上相对让我觉得棘手的,并且有趣的场景。
表单中有很多的下拉选项,但是页面(或者说 Form)并不是一开始就拥有对应 Select 组件的下拉选项数据。
因为下拉选项的数据很可能有很多,而且 B 端的 Form 表单项数量也非常多,如果一开始就获取所有下拉选项数据,将是一件大工程(并非指所有情况)
那么如何异步获取对应下拉项的选项数据,成了我要面临 Formily 的第一个问题。
其实异步获取的接口数据,是 Select 组件的一个 props
所以我们把问题抽象一下,其实是如何通过 Formily 来动态在 Form 初始化改变某一个 FormItem 的 props的问题。
const schema = {
type: "object",
properties: {
linkage: {
type: "string",
title: "联动选择框",
enum: [
{ label: "发请求1", value: 1 },
{ label: "发请求2", value: 2 },
],
"x-decorator": "FormItem",
"x-component": "Select",
"x-component-props": {
style: {
width: 120,
},
},
},
select: {
type: "string",
title: "异步选择框",
"x-decorator": "FormItem",
"x-component": "Select",
"x-component-props": {
style: {
width: 120,
},
},
"x-reactions": ["{{useAsyncOptions(loadData)}}"], // <----- look at here
},
},
};其实需要用到 Formily 的 schema 的一个配置属性x-reaction
x-reaction可以理解为一个 trigger,就是在特定情况下会触发的 reaction 行为。
// 完成formily异步数据获取。也可以处理一些联动异步获取数据的操作
export const useAsyncOptions =
(
service: (
field: FormilyField,
searchValue?: string
) => Promise<OptionItem<SingleSelectType>[]>
) =>
(field: FormilyField) => {
const fieldName = field.props.name as SingleSelectType
useAsyncOptions接收的参数其实就是异步请求接口的 function。上方代码中的loadData可以对应多个 function。
{% raw %}
<SchemaField
schema={schema}
scope={{
useAsyncOptions,
duty_level_options,
corporation_options,
office_address_options,
duty_options,
position_options,
}}
/>
{% endraw %}这个场景相对简单一些,其实就是需要给SchemaField的scope属性进行一些配置,将一些函数放入 Form 的 scope 中,这样 Form 就能拿到对应的方法,在相对应的时机去触发。
后端搜索所所对应的场景是FormItem可能会有很多数据的情况(多发生在 Select 等组件),需要通过输入关键字来进行搜索。
但是有时候并非是前端的搜索,需要通过给后端接口传入搜索关键字来与后端形成交互。
来抽象一下这个问题,和刚才的异步获取数据有些类似,通过 Formily 来实时改变某一个 FormItem 的 props。
{% raw %}
import { action, observable } from "@formily/reactive";
export const useAsyncOptions =
(
service: (
field: FormilyField,
searchValue?: string
) => Promise<OptionItem<SingleSelectType>[]>
) =>
(field: FormilyField
我们还需要用到刚才的x-reaction配置和与其对应的useAsyncOptions。
刚才在异步获取下拉选项数据时,是去调用接口。现在的步骤则是:
而上述代码中useAsyncOptions的参数 service 依然是调取后端接口的 function。我们通过给 form 添加 effect,然后来监听 field 的改变来触发onFieldReact。
而关键词的改变则交给组件本身提供的回调函数即可,我们仅需在onFieldInit中配置好相应的 props 即可。
复合字段是在 form 中是一个FormItem,但是实际上却对应了多个子Item的情况。
比如

图中,现居住地址在 Form 中仅代表一个字段,value也对应的一个值,比如currentAddress: { ... }。
但是显然currentAddress需要多个子字段的值来组成最终的 value 。这个时候就需要子表单。
const currentAddress = {
address: [
{
code: '120000',
name: '天津市',
},
{
code: '120000',
name: '天津市',
},
{
code: '120101',
name: '和平区',
},
],
detail: '华南里28号楼-908',
在本篇文章中,仅给出一种解决方案,就是子表单。
子表单其实就可以单独把这个字段理解成一个表单。而这个表单暴露出去的最终value就是外部表单所对应字段的value。

{% raw %}
const FormAddressDetail: React.FC<FormAddressDetailProps> = (props) => {
const { addressProps, detailProps, detailRequired } = props;
const field = useField<ArrayField>();
// 创建FormAddressDetail内部特有的子表单
其实这个操作相对好理解。因为子表单其实就是一个普通的表单。
我们遇到的问题,是如何将子表单和外部的表单关联起来?特别是校验。
因为我们需要在外部表单校验每一个表单项是否合规时,子表单应该也要触发自身的表单校验。
field.form.addEffects('subFormValidate', () => {
onFieldValidateStart(field.address, async () => {
try {
await form.validate();
} catch (error) {
// 自身是一个子表单,所以不需要父表单的item再显示报错样式
field.setFeedback({
type: 'error',
code: 'ValidateError',
triggerType: 'onInput',
注意到这里,我们需要先拿到当前field对应的外部表单(父表单)。
const outerForm = field.form;然后监听外部表单(父表单)的校验时机onFieldValidateStart。当外部表单校验时,触发子表单的校验
form.validate()我们还需要将父表单的错误通过子表单的错误给覆盖
field.setFeedback({
type: 'error',
code: 'ValidateError',
triggerType: 'onInput',
messages: [''],
});这里有些hack的意思,只能通过message: ['']来将父表单对应Item报错置空。
这样就实现了复合字段在Formily的应用。
其实Formily还提供了一种思路,为后续的拖拽生成表单,乃至页面、搭建PaaS平台提供了一种思路。
至于这一块儿,期待后面的博客吧~