基於 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 作的,其實思想都是同樣的,應用到其餘組件庫同樣的道理,也能夠同時支持多個組件庫,只不過以爲沒有必要。

可能有人會笑,以爲使用配置數據渲染表單沒啥必要,還不如爲所欲爲地拷代碼組裝出來,其實我也是這樣笑過來的。

但願你們能花一點時間嘗試用一下,若是喜歡的話,歡迎交流。

相關文章
相關標籤/搜索