阅读 208

复杂表单解决方案探索

场景

业务中时常会存在复杂表单的场景,比如 “第一项的值” 决定 “第二项的显隐”,“第二项的值” 决定 “第三项的显隐” 这种情况。

这种场景一个普遍的做法是,针对每一个“动态的表单项”,单独设置一个 state 来控制显隐,如下:

// 在该表单中,如果性别选择是“女生”,则显示“是否已结婚”的选项

// 这是一个常规的做法
// 但是不一定是好的做法!!!

class Form {
  state = { sex: null, showIsMarried: false }
  
  handleChangeSex = (sex) => {
    const showIsMarried = sex === 'gril'
    this.setState({ sex, showIsMarried })
  }
  
  render = () => {
    return (
      <Fragment>
        <Select onChange={this.handleChangeSex} data={[
          { text: '男生', value: 'boy' },
          { text: '女生', value: 'girl' }
        ]}></Select>
        {
          this.state.showIsMarried
          &&
          <Select data={[
            { text: '已结婚', value={...} },
            { text: '未结婚, value={...} }
          ]}></Select>
        }
      </Fragment>
    )
  }
}
复制代码

上面是一种常规的做法。

快速通道

如果你暂时不想深入了解,只是想体验下,那么关于这个方案的实现,我已在github上提供了,可以点击 查看zanForm方案

或者想在浏览器里面看下运行的效果,点击查看在线示例。这个示例中,右侧是代码块。

探究

1、是否有必要针对每一个“动态的表单项”,单独设置一个 state 来控制显隐?

答案是:不需要!

看下面的例子:

// 在该表单中,如果性别选择是“女生”,则显示“是否已结婚”的选项

// 将对文首提到的例子做改造:
// 尝试移除 showIsMarried , 来减少复杂度

class Form {
  state = { sex: null }
  
  handleChangeSex = (sex) => {
    this.setState({ sex })
  }
  
  render = () => {
    return (
      <Fragment>
        <Select onChange={this.handleChangeSex} data={[
          { text: '男生', value: 'boy' },
          { text: '女生', value: 'girl' }
        ]}></Select>
        {
          this.state.sex === 'girl'
          &&
          <Select data={[
            { text: '已结婚', value={...} },
            { text: '未结婚, value={...} }
          ]}></Select>
        }
      </Fragment>
    )
  }
}
复制代码

这个方式减少了 state.showIsMarried,直接通过 state.sex 去判断显示和隐藏。

2、如何聚合表单数据为values?

// 在该表单中,如果性别选择是“女生”,则显示“是否已结婚”的选项

// 尝试移除 state,把 表单管理 交给 高阶函数createForm()
// createForm 为 <Form /> 注入 values

class Form {
  render = () => {
    const { values, setValues } = this.props;
    
    return (
      <Fragment>
        <Select onChange={setValues('sex')} data={[
          { text: '男生', value: 'boy' },
          { text: '女生', value: 'girl' }
        ]}></Select>
        {
          values.sex === 'girl'
          &&
          <Select data={[
            { text: '已结婚', value={...} },
            { text: '未结婚, value={...} }
          ]}></Select>
        }
      </Fragment>
    )
  }
}

export default createForm(Form)
复制代码

可以看到,我们把 state.sex 变成了 state.value.sex。这样做不仅减少编写冗余代码,也让表单值很多的情况下,也比较容易维护。

对于这点,业界有现成的方案,如 rc-form。这一步我们无需考虑。只是为了理解的连贯性,顺便一提。

3、为何使用配置化?

jsx 写React,是一种更偏向 视觉 的做法,一种物理模型的拼凑。

在表单场景下,其实是没有太多的 视觉 可言的,大多数情况都是一行一行地排列。它更重的是 逻辑 上的条理。比如各项之间的数据关系。

如果单纯考虑逻辑的话,配置化 无疑是更好的选择。

既然要配置,就需要用对象来对组件做描述。描述的基本两要素是:

组件名表单项名

如下:

// 描述的基本模型
[
  {
    _component: "InputItem",
    _name: "form_item_name",
    _show: values =>
      values.item_a_value === "hello" && values.item_b_value === "world"
  }
];
复制代码

上面的描述的最终生成如下:

// jsx代码
{
  values.item_a_value === "hello" && values.item_b_value === "world" && (
    <InputItem _name="form_item_name" />
  );
}
复制代码

4、为何提供插槽?

并非所有业务场景,都是能够通过 配置化 来编写表单的。

一些特殊需求下,组件还是需要在配置外实例化:如组件会调用到<Form />上的一些方法,或者访问到一些变量。

这时候就需要用到插槽,即 Slot

插槽其实就是 占位。可以用一个 id 来表示待填充物,如下:

// form.config.js
[
  {
    _show: values => values.greet === "go_greet",
    _slot: "im_slot"
  }
];

// App.jsx
zanForm(formConfig, this)(
  <React.Fragment>
    <Slot id="im_slot">
      <div>Hello,World</div>
    </Slot>
  </React.Fragment>
);
复制代码

5、为何提供一种新的获取远程数据的方法?

一些场景中,<Select>options 来自服务端数据。

一个常见的方式是在<Form />componentDidMount 里获取数据 data,并通过 setState({ options: data }) 的方式,去给<Select>赋值。

既然采用了 配置化 ,那么取值就不能再采用这个方式。而是如下:

定义一个获取数据的方法叫做 _fetch_data(),约定返回类型为 Promise,并且 resolve(data) 需要用到的数据,如下:

// form.config.js
[
  {
    _component: "FormSelectField",
    _name: "province",
    _fetch_data: () => axios("/province.json").then(res => res.data),
    label: "省份",
    data: []
  }
];
复制代码

6、其他

当然,在实际业务场景中,需要实现的还远不止以上提到的这些。

比如 _format_subscribe,支持注册外部组件,等等……。因内容比较多,且核心程度不如以上的,就不再赘述。如果想了解更多,可关注 zanForm 项目

关注下面的标签,发现更多相似文章
评论