基于 Ant Design 开发了一个表单配置渲染库,可以帮助你通过配置数据快速渲染一个表单并进行表单操作。
Github:github.com/beyondxgb/a…
Examples: beyondxgb.github.io/afms
- ✔︎开箱即用,基于最受欢迎的 React 组件库 Ant Design,非常容易上手。
- ✔︎数据驱动,对表单的任何操作都可以通过操作配置数据完成。
- ✔︎高维护性,维护表单,只需要维护配置数据。
- ✔︎高扩展性,可以高度定制自己的表单组件和组装表单组件,轻松应付各种定制需求。
- ✔︎状态切换,轻松切换表单组件状态(编辑态、展示态、禁用态)。
- ✔︎复杂布局,具有灵活的布局,可应对各种复杂表单。
前言
在中后台应用中,表单是不可缺少的一部分,相信大家对表单都有一种恐惧感,表单渲染出来比较简单,但是要处理表单联动、表单元素状态(编辑,禁用,显示)、表单各种校验等,代码写出来往往会是一大坨,逻辑遍布各种地方,比较难维护而且代码复用性极差。
使用 Antd 进行表单的处理其实已经提高不少效率,直接拷贝一下官方代码就可以出来一个表单,但还是避免不了前面提到的问题,如何优雅地处理表单还是要进行一层封装才行。
我使用 Antd 处理表单经历过三个阶段:粗暴处理 -> 抽象元素 -> 配置渲染。
粗暴处理
需要什么表单组件、表单布局,直接拷贝代码,刷刷刷就出来一个表单,然后需要什么校验,给每个组件配置上,需要进行表单联动的话,监听一下组件 onChange 事件,再修改一下其他组件的值,如果还需要控制组件的状态(编辑态、显示态),那就单独写一个函数渲染这个组件,在里面根据状态进行渲染, 这里就不贴代码了,相信大家也经历过这个阶段,应该比较有画面感。
抽象元素
表单做多了,发现这样粗暴处理,感觉没有一点点追求,都是重复的工作,而且维护成本高。于是找到一些共性,对常用的表单组件进行一层封装,例如 Input,
import { Input, Form } from 'antd';
const FormItem = Form.Item;
class InputField extends React.Component {
getContent() {
const {
id, value, defaultValue, form, decorator, config,
} = this.props;
if (status === 'edit') {
// 编辑状态
const { getFieldDecorator } = form;
const fieldDecorator = getFieldDecorator(id, {
initialValue: value === undefined ? defaultValue : value,
...decorator,
});
return fieldDecorator(
<Input {...config} />
);
} else if (status === 'preview') {
// 预览状态
return <span className="plain-text">{value}</span>;
}
}
render() {
const { formItem = {} } = this.props;
<FormItem {...formItem} required={status === 'preview' ? false : formItem.required}>
{this.getContent()}
</FormItem>
}
}
做的事情主要是把必要但又繁琐的 FormItem 和 getFieldDecorator 封装起来,不用每次重复写,另一方面就是对表单组件的状态进行处理,区分编辑态和展示态,这样可以方便切换状态。
封装完需要用到的表单组件后,渲染表单就是对这些表单组件进行组装了:
import { Form } from 'antd';
class FormPage extends React.Component {
const { form } = this.props;
return (
<Form>
<InputField id="input" form={form} ... />
<SelectField id="select" form={form} ... />
<DatePicerField id="date" form={form} status="preview" value="2010-10-02" ... />
<OtherField id="other" form={form} ... />
</Form>
);
}
export default Form.create()(FormPage);
经过抽象处理后,处理表单就有点感觉了,在不失灵活性的前提下,代码得到比较高的重用,完全在可控之中。
配置渲染
抽象出各种表单组件后,维护起来确实比粗暴处理好多了,只要维护一个组件库,每个项目都按这样开发表单就好了。但如果只止于此的话,体现不出一名优秀的工程师的气质,感觉这个方案不具有通用性,也不够强大,还有比较大优化空间。
在抽象表单组件的时候,已经有想过使用 json 配置的方式进行渲染,例如:
const fields = [
{ id: 'input', formItem: { label: 'Input' } },
{ id: 'select', formItem: { label: 'Select' } },
{ id: 'datePicker', formItem: { label: 'DatePicker' }, status: 'preview', value: '2010-10-02' },
}];
const FormRender = (props) => {
const { fields, form } = props;
return (
fields.map(item => (
// input
<InputField {...item} form={form} />
// select
<SelectFiel {...item} form={form} />
...
))
);
};
import { Form } from 'antd';
class FormPage extends React.Component {
const { form } = this.props;
return (
<Form>
<FormRender fields={fields} form={form} />
</Form>
);
}
export default Form.create()(FormPage);
但感觉会不够灵活,有几个问题比较担忧的:
- 如何处理表单联动问题?渲染组件都是一个循环的,如何捕获到组件的 onChange 事件?又如何修改其他组件的属性值?
- 如果内置的表单组件不满足需求,业务上如何定制自己的组件?
- 这样能覆盖多少场景?有没有信心面对复杂的表单?
持续了一段时间,没有去思考如何解决这几个问题,后来业务上遇到特别多的表单需求,不得不重新思考下,这几个问题也是可以解的,然后做了一个表单配置渲染库,解决了业务上的问题,经历了半年多的考验,证明思路是对的,才进行了开源与大家交流,也就是 afms,下面简单介绍一下它。
简单介绍
对于表单配置渲染,相信已经有很多人做过了,道理大家都懂,就是约定一份配置格式,然后根据规范渲染出表单元素,但往往都是只能满足简单的场景,而且使用的体验不太友好,可能只能用在搭建简单表单页面的场景。在做之前也调研过市面上做表单配置渲染的库,都不合自己的口味。所以只能自己设计一版,自己用得爽才是硬道理。
在 afms 中,有几个关键概念:
- FormRender: 整个表单的容器,读取配置,进行表单总体布局,托管所有组件的事件,获取表单元素的值。
- FormRenderCore:表单元素渲染器,负责根据配置数据渲染出表单元素,可实时注册表单元素。
- Field:表单元素,组件都是基于 Antd 进行包裹一层,组件的配置和 Antd 保持一致,可定义自己的业务组件。
大概结构代码上演示:
<FormRender
config={formConfig}
wrappedComponentRef={(ref) => { formRef = ref; }}
onChange={handleFormChange}
>
<FormRenderCore>
<Field1 />
<Field2 />
...
</FormRenderCore>
...
</FormRender/>
下面是 formConfig 的配置格式:
{
status: 'edit',
layout: 'horizontal',
labelCol: {
span: 4,
},
wrapperCol: {
span: 10,
},
fields: [{
field: 'input',
id: 'password',
value: '***',
status: 'edit',
formItem: {
label: 'Password',
},
decorator: {
rules: [{
required: true,
message: 'Please input your password',
}],
},
config: {
placeholder: 'password',
},
previewRender: field => field.value,
emptyContent: '-',
}],
}
配置的设计亮点在于无缝对接 Antd Form 和官方组件的属性配置,外层的配置则为 Form 的配置,主要控制表单整体性的东西,如布局、表单项属性配置。
现在来看看 fields 几个配置,
- formItem: Antd Form 里 Form.Item 的配置。
- decorator: Antd Form 里 getFieldDecorator 的配置。
- config: Antd 组件的属性配置,如果为自定义组件,则为自定义组件的属性配置。
在设计上基本沿用 Antd 里的配置,额外的配置用到实现自己想做的功能,主要是增加了表单元素的状态切换(编辑态、展示态、禁用态)和增强了表单布局功能, 所以使用 Antd 搭建出来的表单,都可以写成一份配置数据。
常用功能
下面介绍一下,如果利用 afms 实现表单常用的功能,下面只展示核心代码,详细请查看在线 Examples。
基础渲染
表单的基础处理,主要流程是 定义配置数据 -> 渲染 -> 提交 -> 获取数据,这也是表单配置渲染具有的基本功能,下面看看使用 afms 渲染表单基本的框架:
const formConfig = {
labelCol: { span: 3 },
wrapperCol: { span: 12 },
fields: [
{ field: 'input', id: 'name', formItem: { label: 'Name' } },
...
],
};
let formRef;
export default () => {
function handleSubmit() {
const { form } = formRef.props;
form.validateFields((err, values) => {
...
});
}
return (
<div>
<FormRender
config={formConfig}
wrappedComponentRef={(ref) => {
formRef = ref;
}}
/>
<FormItem wrapperCol={{ span: 18, offset: 3 }}>
<Button type="primary" onClick={handleSubmit}>
Submit
</Button>
</FormItem>
</div>
);
}
详细请查看样例 BasicForm。
表单布局
表单的布局状态除了支持 Antd Form 里的三个 'horizontal' | 'vertical' | 'inline' 外,新增了 'multi-column' 属性,主要支持多列布局,因为很多时候需要两列或者三列,甚至更复杂,和表格的布局类似,有时需要横跨多行、横跨多列,所以加了这个配置。多列布局这个功能我觉得 Antd 可以内置,目前我这里临时做了,主要是表单需求中比较多这样的场景。
const formConfig = {
layout: 'multi-column',
column: 3,
fields: [
{ field: 'input', id: 'name' },
{ field: 'input', id: 'memo', colSpan: 2 }
],
}
这里定义表单有三列,每个表单元素占据三分之一的宽度,但是 memo 定义占据两列,所以它占据了三分之二的宽度。
详细请查看样例 FormLayout 和 ComplexLayout。
表单元素状态
不知大家有没有遇到这样的需求,一个表单,可以支持一直编辑的,即一开始展示已经提交过的数据,点击编辑就可以编辑表单的内容。一般做法可能是写两个模块,一个模块是编辑功能,另一个模块是展示数据的,一开始我也是这样的做的,但这样做两个模块的逻辑是有很大重合的,维护起来也比较麻烦,因为这个需求,才有了上面提到的抽象出表单元素,这样使用的话就可以传 status 属性,根据 status 来渲染不同状态。
<Field status="edit | preview | disable" />
目前 afms 里内置的表单元素都是有三种状态的,编辑态、展示态和禁用态,直接指定即可,默认是 edit 状态。
const formConfig = {
fields: [
{ field: 'input', id: 'name', status: 'edit' },
{ field: 'input', id: 'memo', status: 'preview', value: '1234' },
{ field: 'input', id: 'sex', status: 'disabeld' },
],
}
当然,除了可以独立指定表单元素的状态,也可以全局指定整个表单的状态,全局状态可以被局部的状态覆盖。
const formConfig = {
status: 'edit',
fields: [
{ field: 'input', id: 'name' },
{ field: 'input', id: 'memo', status: 'preview', value: '1234' },
],
}
这时虽然指定了表单状态为 编辑态,但是 memo 这个元素是展示态。
详细请查看样例 FormFieldStatus。
表单联动
表单联动的问题非常常见,真正的表单需求很少有静态的表单。联动的场景比如一个表单元素修改了,会影响另一个表单元素的值。
第一种方法,监听 FormRender 的 onChange 方法,它托管了所以表单元素的 onChange 事件,所以能监听到目标元素的改变,然后通过修改 formConfig 来修改其他元素。
function handleFormChange(item, event) {
switch(item.id) {
case 'name':
// update formConfig
break;
default:
}
}
<FormRender
config={formConfig}
wrappedComponentRef={(c) => {
formRef = c;
}}
onChange={handleFormChange}
/>
第二种方法,直接在 json 配置数据里定义 filed 的 onChange 事件,通过 form.setFieldValue 来改变其他元素。
const formConfig = {
fields: [
{
field: 'input',
id: 'name',
config: {
onChange(form, event) {
// form.setFieldValue
}
}
},
],
}
详细请查看样例 FormLinkage。
表单组合
有这样一个场景,一个表单是由多个模块组成的,如何使用配置描述?这时可以看做是多个表单,每个表单可以独立渲染,但表单的数据控制还是有一个整体的容器。
import { FormRender, FormRenderCore } from 'afms';
export defualt () => (
<FormRender
wrappedComponentRef={(ref) => {
formRef = ref;
}}
>
<h3>BaseInfo</h3>
<FormRenderCore
config={form1Config}
/>
<h3>MoreInfo</h3>
<FormRenderCore
config={form2Config}
/>
</FormRender>
);
前面也提到,FormRenderCore 是表单渲染器,FormRender 只是表单的容器,如果直接在 FormRender 里指定配置数据的话,FormRender 默认渲染一个 FormRenderCore,不指定配置数据的话,你可以在它内部使用 FormRenderCore 随意渲染表单,收集表单数据还是由 FormRender 来收集,这样就可以实现多个表单组合的情况了。
详细请查看样例 MutipleForm。
表单组装
在前期评估中,如果觉得这表单需求,使用配置数据进行渲染,会有限制,满足不了某种需求,则可以回归到原始的办法,表单元素组装!
import { FormRender, InputField } from 'afms';
export default () => (
<FormRender
config={formConfig}
wrappedComponentRef={(c) => {
formRef = c;
}}
>
<InputField id="name" formItem={{ label: 'Name' }} />
<InputField id="memo" formItem={{ label: 'Memo' }} />
...
</FormRender>
);
这个方法是一个万能的方法,不是做 afms 的初衷,但还是能提供了一个选择,可以不使用配置数据进行表单渲染。
详细请查看样例 AssembleFormField。
自定义表单元素
除非业务非常简单,内置的表单元素已经足够用来渲染表单,但真实情况肯定是不会满足的,这时配置就需要支持自定义自己的表单元素。
在 fields 的配置中,field 的值可以字符串或者是一个组件,如果是字符串,则是定义内置的表单元素,如果是一个组件,则是定义自己的表单元素:
import PriceInputField from 'components/PriceInputField';
const formConfig = {
fields: [
{
// field: 'input',
field: PriceInputField,
id: 'name',
config: {},
...
},
],
}
自定义自己的表单元素也是有规范的,可以继承 BaseField,然后实现自己的方法即可:
import React from 'react';
import { BaseField } from 'afms';
import PriceInput from './PriceInput';
export default class PriceInputField extends BaseField {
getComponent = () => {
const { config } = this.props;
return <PriceInput {...config} />;
};
getPreviewStatus = () => {
const { value } = this.props;
const { number, currency } = value;
return <span className="plain-text">{number} {currency}</span>;
};
getDisabledStatus = () => null;
getReadOnlyStatus = () => null;
}
详细请查看样例 CustomFormField。
注册表单元素
注册表单元素,主要是定义 field 的类型:
import { FormRenderCore } from 'afms';
import PriceInputField from 'components/PriceInputField';
FormRenderCore.registerFormFields({
'price-input': PriceInputField,
});
这样就可以全局定义好 field 的类型, 这样在配置中 field 字段保持是字符串,这里既可以注册自定义的表单元素,也可以覆盖内置的表单元素。
提供这个功能,一方面主要优化使用体验,全局注册好的话,就不用在每次的配置都需要引用自定义表单元素,直接配置 field 的类型即可。
另一方面主要是考虑到一个场景,如果是团队合作的话,有很多业务的表单组件需要进行共用,那有两种方法,
- 建立公共的组件库,每个项目需要的话直接注册即可。
- 建立自己团队的表单配置渲染库,底层引用的是 afms,里面定义业务的表单元素。
满足这种场景,注册表单元素这个功能显得非常有必要。
结语
使用 afms 渲染表单,不敢说能满足100%的表单需求,但非常有自信地说能满足99%的表单需求,因为表单组装那个方法是万能的,剩下那1%不满足可能就是个人选择偏好了。
虽然是基于 Antd 做的,其实思想都是一样的,应用到其他组件库一样的道理,也可以同时支持多个组件库,只不过觉得没有必要。
可能有人会笑,觉得使用配置数据渲染表单没啥必要,还不如随心所欲地拷代码组装出来,其实我也是这样笑过来的。
希望大家能花一点时间尝试用一下,如果喜欢的话,欢迎交流。