最近研究动态表单,分享下如下Form设计。
React-UI 0.6 Form设计
0.6版最大的变化,把整个Form架构重新写了一遍,差不多覆盖了整个UI库的2/3。
0.6之前的Form,是这样一个结构。
使用的时候是这样的
<Form layout="aligned" onSubmit={...} data={...}>
<FormControl name="text" label="text" type="text" min={2} max={6} />
<FormControl name="email" label="email" type="email">
<span className="rct-input-group">
<span className="addon"><Icon icon="email" /></span>
<Input type="email" />
</span>
</FormControl>
...
</Form>
这样设计是想把所有的表单组件注册到FormControl,由FormControl实现validate,生成tip文字,和Form交互的工作。不过由于写这个UI库的时候,接触React时间还不长,有些思想还是停留在Angularjs和Vue的双向绑定上,另外也没有想到好的办法解决获取Form表单数据的问题。所以给所有的表单组件加上了getValue和setValue两个方法进行传值。
这个设计实现了我开始想实现的大部分功能,但是很快发现不太够用了。很多表单都会有一两个比较复杂的交互,单纯通过FormControl的props很难描述,通过使用children这样的方式,也非常吃力,因为一个FormControl只能描述一个FormData字段,如果一行有多个表单组件,就不好用了。另外,由于Form内使用了cloneElement,在最外层也无法拿到原始的ref。
最致命的是,在使用mixin的时候,使用getValue和setValue不会有多大的问题。但是,后来大部分组件都采用了higherorder component,来附加一些通用的功能,比如取服务端数据用的Fetch。这样FormControl就无法直接通过setValue和getValue来操作数据了。虽然可以通过改变higherorder来支持,但是这样显然是不对的。
所以,在0.6的时候,决心对Form进行一次重构。新的架构是这样的。
最大的变化是增加了两个FormItem,一个是higherorder的组件,用来包装input类的表单组件,实现了原来FormControl的validate的功能。另外一个是对这个higherorder组件的封装,暴露出来供使用者调用。
原先的FormControl职责变得简单一些,不再处理数据,只用来生成label文字,汇总FormItem子组件(一个FormControl下可以有多个FormItem)的状态,生成相应的提示/错误信息。
最困难的是Form的部分。数据向下传递比较简单,要在onSubmit的时候获取到所有表单项的状态:是否通过校验,这个比较麻烦。在一个FormItem没有触发onChange的情况下,Form如何知道这个FormItem存在,并让它去做validate校验?如果通过了校验,怎么拿到初始值?
想了好几种解决方案,都不太理想。最终决定,增加了一个itemBind机制。
this.itemBind = (item) => {
this.items[item.id] =item;
let data = this.state.data;
data[item.name] = item.value;
this.setState({ data });
// bind triger item
if (item.valiBind) {
item.valiBind.forEach((vb) => {
this.validationPools[vb] = (this.validationPools[vb] || []).concat(item.validate);
});
}
};
this.itemUnbind = (id, name) => {
let data = this.state.data;
delete this.items[id];
delete data[name];
delete this.validationPools[name];
this.setState({ data });
};
this.itemChange = (id, value, err) => {
let data = this.state.data;
const name = this.items[id].name;
if (data[name] !== value) {
data[name] = value;
this.setState({ data });
}
let valiBind = this.validationPools[name];
if (valiBind) {
valiBind.forEach((validate) => {
if (validate) {
validate();
}
});
}
this.items[id].$validation = err;
};
}
每个FormItem初始化的时候直接bind到Form的items,数据改变的时候,通过itemChange方法,直接把数据和状态提交到Form的formData。Form在submit的时候,检查items里面每个item是否通过validate,如果没有执行过validate,通知item执行。如果所有items状态都ok,再触发onSubmit。
这一切对于使用者来说是黑盒的。对外的Api基本上是没有变的。
一个完整的Form调用现在是这样。
<Form data={...} onSubmit={...}>
<FormControl label="label">
原生input:
<FormItem name="filed1" value="初始值" min={2} max={12} validator={...}>
<input />
</FormItem>
自定义组件,需要支持value传入值,onChange传出值
<FormItem>
<CustomComponent {...} />
</FormItem>
Input组件
<Input name="filed2" type="url" />
</FormControl>
</Form>
如果FormControl只有一个组件,可以写成这样,这样就和之前的版本没有区别了。
<Form data={...} onSubmit={...} layout="...">
<FormControl label="label" name="filed" type="text" required min={2} max={12} validator={...} />
</Form>
动态表单
这个是一直想实现的功能,0.6里也顺带完成了,通过一个json格式,动态生成一个表单。
<Form button="确定" fetch={’json/form.json’} controls={[
{ name: ’text’, type: ’text’, min: 3, max: 12, label: ’text’, grid: 1/3 },
{ name: ’datetime’, required: true, type: ’datetime’, label: ’datetime’, tip: ’自定义tip文字’ },
{ label: ’two items’, items: [
{ name: ’startTime’, type: ’date’ },
’-’,
{ name: ’endTime’, type: ’date’ }
] },
{
name: ’select’, type: ’select’, label: ’select’, grid: 1/2, fetch: {url:"json/countries.json", cache:3600},
mult: true, filterAble: true, valueTpl: "{en}",
optionTpl: ’<img src="//lobos.github.io/react-ui/images/flags/{code}.png" /> {country}-{en}’
}
]} />
实现这一步比较简单了,因为原本Form所有的children都是FormControl,只要把controls遍历一下,return <FormControl {…control} />就好了
来自: https://github.com/Lobos/react-ui/issues/43