on
Formily取经
最近的工作涉及到了很多表单,也在不知不觉中意识到了自己对表单的一无所知。
今天,就来聊聊Formily
。
Fomily 为什么诞生
Formily 只是众多表单处理方式里的一种。
我们必须要先说说表单本身。
先说说表单本身
最初我对表单的理解可能是这个样子的
但是实际的 Form 表单场景却是
这样的
这样的
等等…
作为一个 ToB 的前端开发,从我的角度去看,Form 表单在 B 端的使用是相当灵活且广泛的。
上图给出的场景,仅仅是表单形式
的不同。
Form 场景的复杂还体现在
- Form 内字段关联逻辑复杂(一对多 / 多对一 / 多对多)。
- 一些 FormItem 的 value 操作与转换
- 不同端不想再写一遍 Form!能不能一份 Form 多端使用。
- 既然多端使用了,布局的问题怎么办!!!
- 表单的编辑态 / 预览态
等等…
一些 Formily 的使用场景
通过一个多月对 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": [""], // <----- 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;
if (field.dataSource?.length) {
return;
}
if (SINGLE_SELECT[fieldName]) {
service(field).then(
action.bound?.((selectOptions) => {
const tempProps = {
options: selectOptions,
labelKey: DEFAULT_LABEL_VALUE_KEY[fieldName].labelKey,
valueKey: DEFAULT_LABEL_VALUE_KEY[fieldName].valueKey,
};
// 移动端需要添加drawer header
if (isMobile()) {
Object.assign(tempProps, {
headerProps: { title: field.title || "请选择", canClose: true },
});
}
field.setComponentProps(tempProps);
})
);
}
};
useAsyncOptions
接收的参数其实就是异步请求接口的 function。上方代码中的loadData
可以对应多个 function。
<SchemaField
schema={schema}
scope={{
useAsyncOptions,
duty_level_options,
corporation_options,
office_address_options,
duty_options,
position_options,
}}
/>
这个场景相对简单一些,其实就是需要给SchemaField
的scope
属性进行一些配置,将一些函数放入 Form 的 scope 中,这样 Form 就能拿到对应的方法,在相对应的时机去触发。
后端搜索
后端搜索所所对应的场景是FormItem
可能会有很多数据的情况(多发生在 Select 等组件),需要通过输入关键字来进行搜索。
但是有时候并非是前端的搜索,需要通过给后端接口传入搜索关键字来与后端形成交互。
来抽象一下这个问题,和刚才的异步获取数据有些类似,通过 Formily 来实时改变某一个 FormItem 的 props。
import { action, observable } from "@formily/reactive";
export const useAsyncOptions =
(
service: (
field: FormilyField,
searchValue?: string
) => Promise<OptionItem<SingleSelectType>[]>
) =>
(field: FormilyField) => {
const fieldName = field.props.name as SingleSelectType;
// 获取当前field对应的Form表单对象
const form: Form = field.form;
// 通过生成一个ref来记录搜索的关键词
const keyword = observable.ref("");
// 给form添加副作用处理(也可以在createForm的时候来进行)
form.addEffects(field.address, () => {
// Field初始化的时候要做的事情
onFieldInit(field.address, () => {
// 职位需要支持搜索
if (fieldName === SINGLE_SELECT_TYPE.JOB_POSITIONS) {
field.setComponentProps({
searchable: true,
onSearch: debounce((value: string) => {
keyword.value = value;
}, 200),
});
}
});
// 当Field一些内容发生了改变的时候(这里是关键词)
onFieldReact(field.address, () => {
service(field, keyword.value).then(
action.bound?.((selectOptions) => {
field.setComponentProps({ options: selectOptions });
})
);
});
});
};
我们还需要用到刚才的x-reaction
配置和与其对应的useAsyncOptions
。
刚才在异步获取下拉选项数据时,是去调用接口。现在的步骤则是:
- 用户输入关键词,并记录
- 调取后端接口,参数携带关键词
- 后端返回对应的数据,需要更新 props
而上述代码中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。
const FormAddressDetail: React.FC<FormAddressDetailProps> = (props) => {
const { addressProps, detailProps, detailRequired } = props;
const field = useField<ArrayField>();
// 创建FormAddressDetail内部特有的子表单
const form = useMemo(
() =>
createForm({
effects() {
onFormValuesChange(() => {
const { address = [], details = '' } = form.values;
const newAddress = address.map((item: ExposeValueItem) => {
return {
code: item[KEY_MAP.VALUE],
name: item[KEY_MAP.LABEL],
};
});
field.value = [...newAddress, details];
});
},
}),
[field]
);
// 需要在外部表单校验的同时校验内部表单
useEffect(() => {
field.form.addEffects('subFormValidate', () => {
onFieldValidateStart(field.address, async () => {
try {
await form.validate();
} catch (error) {
// 自身是一个子表单,所以不需要父表单的item再显示报错样式
field.setFeedback({
type: 'error',
code: 'ValidateError',
triggerType: 'onInput',
messages: [''],
});
}
});
});
}, [field, form]);
// 子表单对应的schema
const schema = useMemo(() => {
return {
type: 'object',
properties: {
address: {
required: field.required,
default: field.value?.filter((item) => typeof item === 'object'),
'x-decorator': 'FormItem',
'x-decorator-props': {
safeArea: 0,
style: {
paddingTop: 0,
paddingBottom: 8,
},
},
'x-component': 'FormAddressSelect',
'x-component-props': { ...addressProps },
},
details: {
type: 'string',
required: detailRequired,
default: field.value?.find((item) => typeof item === 'string'),
'x-decorator': 'FormItem',
'x-decorator-props': {
safeArea: 0,
style: {
paddingTop: 0,
},
},
'x-component': 'FormTextarea',
'x-component-props': { ...detailProps },
},
},
};
}, [addressProps, detailProps, field.required, detailRequired, field.value]);
return (
<FormProvider form={form}>
<SchemaField schema={schema} />
</FormProvider>
);
};
其实这个操作相对好理解。因为子表单其实就是一个普通的表单。
我们遇到的问题,是如何将子表单和外部的表单关联起来?特别是校验。
因为我们需要在外部表单校验每一个表单项是否合规时,子表单应该也要触发自身的表单校验。
field.form.addEffects('subFormValidate', () => {
onFieldValidateStart(field.address, async () => {
try {
await form.validate();
} catch (error) {
// 自身是一个子表单,所以不需要父表单的item再显示报错样式
field.setFeedback({
type: 'error',
code: 'ValidateError',
triggerType: 'onInput',
messages: [''],
});
}
});
});
注意到这里,我们需要先拿到当前field对应的外部表单(父表单)。
const outerForm = field.form;
然后监听外部表单(父表单)的校验时机onFieldValidateStart
。当外部表单校验时,触发子表单的校验
form.validate()
我们还需要将父表单的错误通过子表单的错误给覆盖
field.setFeedback({
type: 'error',
code: 'ValidateError',
triggerType: 'onInput',
messages: [''],
});
这里有些hack的意思,只能通过message: ['']
来将父表单对应Item报错置空。
这样就实现了复合字段在Formily的应用。
除了表单还有什么?
其实Formily还提供了一种思路,为后续的拖拽生成表单,乃至页面、搭建PaaS平台提供了一种思路。
至于这一块儿,期待后面的博客吧~