场景
业务中时常会存在复杂表单的场景,比如 “第一项的值” 决定 “第二项的显隐”,“第二项的值” 决定 “第三项的显隐” 这种情况。
这种场景一个普遍的做法是,针对每一个“动态的表单项”,单独设置一个 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 项目