基於 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 作的,其實思想都是同樣的,應用到其餘組件庫同樣的道理,也能夠同時支持多個組件庫,只不過以爲沒有必要。
可能有人會笑,以爲使用配置數據渲染表單沒啥必要,還不如爲所欲爲地拷代碼組裝出來,其實我也是這樣笑過來的。
但願你們能花一點時間嘗試用一下,若是喜歡的話,歡迎交流。