基于 Ant Design 开发了一个表单配置渲染库,能够帮助你经过配置数据快速渲染一个表单并进行表单操做。javascript
Github:github.com/beyondxgb/a…java
Examples: beyondxgb.github.io/afmsreact
在中后台应用中,表单是不可缺乏的一部分,相信你们对表单都有一种恐惧感,表单渲染出来比较简单,可是要处理表单联动、表单元素状态(编辑,禁用,显示)、表单各类校验等,代码写出来每每会是一大坨,逻辑遍及各类地方,比较难维护并且代码复用性极差。git
使用 Antd 进行表单的处理其实已经提升很多效率,直接拷贝一下官方代码就能够出来一个表单,但仍是避免不了前面提到的问题,如何优雅地处理表单仍是要进行一层封装才行。github
我使用 Antd 处理表单经历过三个阶段:粗暴处理 -> 抽象元素 -> 配置渲染。json
粗暴处理bash
须要什么表单组件、表单布局,直接拷贝代码,刷刷刷就出来一个表单,而后须要什么校验,给每一个组件配置上,须要进行表单联动的话,监听一下组件 onChange 事件,再修改一下其余组件的值,若是还须要控制组件的状态(编辑态、显示态),那就单独写一个函数渲染这个组件,在里面根据状态进行渲染, 这里就不贴代码了,相信你们也经历过这个阶段,应该比较有画面感。markdown
抽象元素antd
表单作多了,发现这样粗暴处理,感受没有一点点追求,都是重复的工做,并且维护成本高。因而找到一些共性,对经常使用的表单组件进行一层封装,例如 Input,app
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);复制代码
但感受会不够灵活,有几个问题比较担心的:
持续了一段时间,没有去思考如何解决这几个问题,后来业务上遇到特别多的表单需求,不得不从新思考下,这几个问题也是能够解的,而后作了一个表单配置渲染库,解决了业务上的问题,经历了半年多的考验,证实思路是对的,才进行了开源与你们交流,也就是 afms,下面简单介绍一下它。
对于表单配置渲染,相信已经有不少人作过了,道理你们都懂,就是约定一份配置格式,而后根据规范渲染出表单元素,但每每都是只能知足简单的场景,并且使用的体验不太友好,可能只能用在搭建简单表单页面的场景。在作以前也调研过市面上作表单配置渲染的库,都不合本身的口味。因此只能本身设计一版,本身用得爽才是硬道理。
在 afms 中,有几个关键概念:
大概结构代码上演示:
<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 几个配置,
在设计上基本沿用 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 渲染表单,不敢说能知足100%的表单需求,但很是有自信地说能知足99%的表单需求,由于表单组装那个方法是万能的,剩下那1%不知足可能就是我的选择偏好了。
虽然是基于 Antd 作的,其实思想都是同样的,应用到其余组件库同样的道理,也能够同时支持多个组件库,只不过以为没有必要。
可能有人会笑,以为使用配置数据渲染表单没啥必要,还不如为所欲为地拷代码组装出来,其实我也是这样笑过来的。
但愿你们能花一点时间尝试用一下,若是喜欢的话,欢迎交流。