React-UI 0.6 Form设计

#1

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>

demo见这里

动态表单

这个是一直想实现的功能,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} />就好了

在线的示例

另外,还尝试写了一个FormBuilder,还有些需要完善的地方,暂时没时间处理,等我搞完0.7,解决了theme的问题再完善吧。

1 Like
React不用Form表单如何优雅的获取input或者div里面的值
#2

加油!我也在开发维护一个form项目:


欢迎沟通交流。