基于 Ant Design 进行表单配置渲染

基于 Ant Design 开发了一个表单配置渲染库,能够帮助你经过配置数据快速渲染一个表单并进行表单操做。javascript

Github:github.com/beyondxgb/a…java

Examples: beyondxgb.github.io/afmsreact

  • ✔︎开箱即用,基于最受欢迎的 React 组件库 Ant Design,很是容易上手。
  • ✔︎数据驱动,对表单的任何操做均可以经过操做配置数据完成。
  • ✔︎高维护性,维护表单,只须要维护配置数据。
  • ✔︎高扩展性,能够高度定制本身的表单组件和组装表单组件,轻松应付各类定制需求。
  • ✔︎状态切换,轻松切换表单组件状态(编辑态、展现态、禁用态)。
  • ✔︎复杂布局,具备灵活的布局,可应对各类复杂表单。

前言

在中后台应用中,表单是不可缺乏的一部分,相信你们对表单都有一种恐惧感,表单渲染出来比较简单,可是要处理表单联动表单元素状态(编辑,禁用,显示)、表单各类校验等,代码写出来每每会是一大坨,逻辑遍及各类地方,比较难维护并且代码复用性极差。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> } }复制代码

作的事情主要是把必要但又繁琐的 FormItemgetFieldDecorator 封装起来,不用每次重复写,另外一方面就是对表单组件的状态进行处理,区分编辑态展现态,这样能够方便切换状态。

封装完须要用到的表单组件后,渲染表单就是对这些表单组件进行组装了:

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);复制代码

但感受会不够灵活,有几个问题比较担心的:

  1. 如何处理表单联动问题?渲染组件都是一个循环的,如何捕获到组件的 onChange 事件?又如何修改其余组件的属性值?
  2. 若是内置的表单组件不知足需求,业务上如何定制本身的组件?
  3. 这样能覆盖多少场景?有没有信心面对复杂的表单?

持续了一段时间,没有去思考如何解决这几个问题,后来业务上遇到特别多的表单需求,不得不从新思考下,这几个问题也是能够解的,而后作了一个表单配置渲染库,解决了业务上的问题,经历了半年多的考验,证实思路是对的,才进行了开源与你们交流,也就是 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

表单联动

表单联动的问题很是常见,真正的表单需求不多有静态的表单。联动的场景好比一个表单元素修改了,会影响另外一个表单元素的值。

第一种方法,监听 FormRenderonChange 方法,它托管了因此表单元素的 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 的类型便可。

另外一方面主要是考虑到一个场景,若是是团队合做的话,有不少业务的表单组件须要进行共用,那有两种方法,

  1. 创建公共的组件库,每一个项目须要的话直接注册便可。
  2. 创建本身团队的表单配置渲染库,底层引用的是 afms,里面定义业务的表单元素。

知足这种场景,注册表单元素这个功能显得很是有必要。

结语

使用 afms 渲染表单,不敢说能知足100%的表单需求,但很是有自信地说能知足99%的表单需求,由于表单组装那个方法是万能的,剩下那1%不知足可能就是我的选择偏好了。

虽然是基于 Antd 作的,其实思想都是同样的,应用到其余组件库同样的道理,也能够同时支持多个组件库,只不过以为没有必要。

可能有人会笑,以为使用配置数据渲染表单没啥必要,还不如为所欲为地拷代码组装出来,其实我也是这样笑过来的。

但愿你们能花一点时间尝试用一下,若是喜欢的话,欢迎交流。

相关文章
相关标签/搜索