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

5,448 阅读13分钟

基于 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>
  }
}

做的事情主要是把必要但又繁琐的 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 做的,其实思想都是一样的,应用到其他组件库一样的道理,也可以同时支持多个组件库,只不过觉得没有必要。

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

希望大家能花一点时间尝试用一下,如果喜欢的话,欢迎交流。