一、背景
前端开发过程中,往往会遇到很多的表单。简单表单尚可,但复杂表单让人尤为头疼。
比如有一个用来提交 请假单申请
的表单。
第一个表单项是 Radio,为性别:分别是 男
和 女
两种选项。
第二个表单项是个 Select,为请假原因:分别为 事假
、年假
、调休
、病假
这四种选项。另外,当上一个性别选项,选择为 女
后,则额外增加一个 产假
的选项。
第三个表单项是个 Uploader,为图片上传组件,用于上传医院证明:该项仅在请假原因选择为 病假
或 产假
后显示,其余情况不显示。
这个场景已经略微有点复杂度了。很多时候如果不好好设计,将写出不好维护的代码。
那如果更加复杂的场景呢?
有赞云业务中,这种场景非常常见,于是推出了 zan-form
来解决这个问题。
二、配置式
对于上面提到的场景,先思考下,用普通方式怎么写。用普通的方式写一遍后,会发现有点难受。为什么呢?
因为 jsx
,本质上是现实中的物理模型,适合像积木一样,去拼凑出各种各样的 UI。
它一旦加上复杂的判断逻辑后,就会很杂乱,需要花大量精力去整理和修饰。
再想一下 配置式
,第一直觉是什么?是逻辑和规则。
表单跟一般的 UI 不一样,尤其复杂的表单,它特别重逻辑,但是视觉复杂度上并不高,不会存在特别多层级的嵌套。所以用配置式来写复杂表单,也许是一个更好的方案。
下面是用配置式表单解决上面提到的场景:
[
{
_component: "FormRadioGroupField",
_name: "sex",
data: [
{
text: "男",
value: "male"
},
{
text: "女",
value: "female"
}
]
},
{
_component: "FormSelectField",
_name: "qingjiaType",
data: [
{
text: "事假",
value: "shijia"
},
{
text: "年假",
value: "nianjia"
},
{
text: "调休",
value: "tiaoxiu"
},
{
text: "病假",
value: "bingjia"
},
{
text: "产假",
value: "chanjia"
}
]
},
{
_component: "ImageUploader",
_name: "hospitalMaterial",
_show: values => ["bingjia", "chanjia"].includes(values.qingjiaType),
tokenUrl: "http://somecdn.youzan.com"
}
];
在上面的配置代码中,_component
和 _name
就不多说了,解释下 _show
方法。
_show
目前仅支持传入一个同步函数,其中入参为 values
。
vlaues
等于 zentForm.getFormValues()
所取得的值。
_show
的出参为一个布尔值,当 true
时,表示该组件显示;当为 false
时,表示该组件不显示。
总体来说,就是把一份配置文件,做为一个数组进行遍历。当遇到 _show
方法时,执行之,并根据其 返回值,决定是实例化的这个组件,还是直接返回 null
。
三、禁用 children
配置式组件,是否应该支持 children
,这是一个有争议性的问题。
支持后有利有弊,但最终还是决定禁止 children
的使用。
原因是,一旦支持实现 children
后,整个配置就偏视觉,而非偏逻辑了。
所以,以下方式是不被允许的:
// 下面的写法,会报错。因为不被允许使用 children
{
_component: 'FormRadioGroupField',
_name: 'sex',
children: [
{
_component: 'Radio',
value: 'male',
text: '男'
},
{
_component: 'Radio',
value: 'female',
text: '女'
}
]
},
当然,为了方便使用,已经对 zent
自带的 FormCheckboxGroupField
和 FormRadioGroupField
这两个组件进行了封装。封装后用法跟 FormSelectField
类似,仅需要传入 [{ text: 'text', value: 'value' }]
即可。如下所示:
// 封装后的 FormRadioGroupField 用法
{
_component: "FormRadioGroupField",
_name: "sex",
data: [
{
text: "男",
value: "male"
},
{
text: "女",
value: "female"
}
]
},
所以对自己写的表单组件,如果需要用到 children
属性,会遇到一些问题,需要做一些改造。
当然,也有其他方式可以绕过,就是后面小节会提到的 _slot
。
四、注册自定义表单组件
到目前为止,能够直接在配置文件中使用的组件,都是 zent.Form
下的组件。
那么对于自定义组件怎么处理呢?
当前在 zan-form
内部维护了一个 componentLib
对象。
在该对象中,key
是组件名,value
是 Component。当解析配置文件的时候,会根据 _component
字段,去找到对应的组件,并实例化它。
所以我们只需要想办法,在 componentLib
中放入我们自定义的组件就可以了。
zan-form
提供了 zanForm.register('ComponentName', MyComponent)
方法,可以在 componentLib
中注册我们自己的组件。
注意,第三节也提到过,如果自定义组件需要用到 children
属性,需要对该组件进行改造,因为当前的配置式是不支持 children
的。
为了能在 zentForm 中更好地使用,自定义组件需要暴露 value
属性,以及支持 onChange
方法做为回调,并用 zent.Form.Filed
包裹。
五、插槽
某些场景下,纯粹的配置式无法满足需求,则需要用 插槽
来实现扩展了。
插槽
的使用如下:
// form.config.js
[
{
_slot: "ImageUploader"
},
{
_slot: "MyFooter"
}
];
// Form.jsx
import zanForm from "zan-form";
import formConfig from "./form.config.js";
const Slot = zanForm.Slot;
class Form extends Component {
render = () => {
return zanForm(formConfig, this)(
<React.Fragment>
<Slot id="ImageUploader">
<ImageUploader>点击上传图片</ImageUploader>
</Slot>
<Slot id="MyFooter">
<Footer>我是页脚</Footer>
</Slot>
</React.Fragment>
);
};
}
如何使用 插槽
,总结起来就是两个步骤:
首先,在配置中,选择合适的点,预留一个插槽。
然后,在配置外的 zanForm
中,使用 Slot
定义一个 待插入组件
,并赋予唯一 id。
注意,_slot
可以跟 _show
一起使用,但是其他的,例如 _fetch_data
、_format
等,一律不支持。
再说下 _slot
的实现原理,其实也很简单:
在 zan-form
内部维护有一个 slotMap
对象,以 Slot.id
做为 key
,Slot.children
做为 value
。当遍历配置文件时,遇到 _slot
,则去 slotMap
里面取对应的 Slot.children
,并填充进插槽即可。
六、与服务端的交互
在日常开发中,存在着大量与服务端交互的场景。
比如 FormSelectField
中的 data
,需要从服务端获取。
一般的方式是,在 componentDidMount
中获取数据,并且通过 setState
塞入到 FormSelectField
的 data
中去。
然而在配置文件中,这似乎很难实现。那么在配置式中,怎么样获取远程数据呢?
目前可以通过 _fetch_data
方法来返回一个 Promise
的方式来实现,如下所示:
// "店铺类型"的Select
{
_component: 'FormSelectField',
_name: 'shopType',
_fetch_data: () => {
return getShopType().then(items => {
return items.map(item => {
return {
text: item.desc,
value: item.type,
};
});
});
},
data: [],
},
以下两点值得注意:
1、原组件(如 FormSelectField 组件)必须支持 data
属性。因为获取到的数据,会默认塞入到 data
中去。
2、_fetch_data
只会触发一次。
实现方式:
这个特性,目前是通过在【原组件】外面,又另外包裹了一层 DecoratorCoponent
实现的。
当 DecoratorComponent
触发 componentDidMount
的时候,就去调用 _fetch_data
,并将 data
通过 props
传给【原组件】。
也就解释了上面提到的,为什么 _fetch_data
只会触发一次。
七、重启组件
存在一种场景,如下:
第一行表单项,是一个 FormSelectField
,代表省份。
第二行表单项,也是一个 FormSelectField
,代表城市。但是该项会根据省份 id,动态从服务端获取城市数据。也就是说,上一项省份改变时,该项城市列表也会改变。
根据上述第六节提到的,如果只用 _fetch_data
,那么根据省份 id 获取城市列表数据,只会触发一次,不会再触发第二次。
所以提出了一个 重启
组件的概念。
重启组件的内部实现,实际上分为两个步骤:
第一步,在 DecoratorCoponent
中改变【原组件】的 key
来重启原组件。
第二步,重新触发自身的 _fetch_data
函数,重新将 data
通过 props
传给【原组件】。
如下所示:
[
{
_component: "FormSelectField",
_name: "province",
required: "请选择您所在省份",
data: [{ text: "浙江省", value: 30 }, { text: "广东省", value: 31 }]
},
{
_component: "FormSelectField",
_name: "city",
_fetch_data: values => {
if (!values.province) {
return Promise.resolve([]);
} else {
return fetchCityByProvince(values.province);
}
},
_subscribe: (prevValues, values, restart) => {
if (values.province !== prevValues.province) {
restart();
}
},
required: "请选择您所在城市",
data: []
}
];
欲 重启
,需先 订阅
。使用 _subscribe
可订阅。
_subscribe
方法中接受三个入参,分别是上一个状态的 values
,本状态的 values
,以及一个用来重启组件的 restart
方法。
比较两次状态的 values.province
,如果不同,则触发重启。
至于 _subscribe
,其实是借助 DecoratorCoponent
中的 componentDidUpdate
实现的。
八、格式化组件
有时候,需要对组件做一些样式上的修改,那么在配置式中,怎么做呢?
目前提供了一个 _format
方法来实现这一点。
如下所示:
[
{
_component: "FormInputField",
_name: "age",
_format: ($component, values) => (
<div>
{$component}
<span>岁</span>
</div>
),
label: "年龄"
}
];
format
方法中,\$component 是组件实例,values 同 _show
方法的 values
。它们做为参数传入,最后返回一个改动后的组件实例。
_format
方法的内部实现,跟 _show
类似。
九、表单回填
一些场景下,比如编辑的时候:需要当从服务端取回数据,并在表单上回显。
这种场景该怎么做呢?
zan-form
提供了一个zanForm.setValues(values, this)
方法,即可把数据回填回去。
本质上,setValues
方法,是对 zentForm.setFieldsValue()
的一个递归调用,最终使 zentForm.getFormValues()
的值趋向于稳定。
十、有赞云招人
有赞云大量 hc 招聘前端,有兴趣的可以加微信 socialHunter
咨询!