背景
上週我寫了一篇文章: 基於React的表單開發的分析(上), 主要講解我們在後臺系統開發中 關於新建、編輯、詳情這三個頁面的異同點以及開發的要點,並最後有提到這期總結一個基於Antd的表單公用元件的設計與實現。
要點
此元件應該具有以下功能:
- 元件接收:需要渲染表單的欄位、初始資料、欄位的控制元件型別等
- 能根據欄位的不同的控制元件型別渲染不同的表單控制元件
- Select
- Input
- ...
- 詳情頁也能複用這個元件
- 具有可擴充套件性(比如Antd的API的方法在此元件中均能使用)
程式碼組織結構
ZHForm => 我暫且這麼叫吧,此元件接收資料來源(預設從欄位fieldDecoratorConfig的initialValue或者dataSource取值,優先順序 initialValue > dataSource)
getFormItem => 一個函式,它的作用是根據控制元件型別和配置返回控制元件,ZHForm 的實現會依賴它
TextPreview => 自定義的表單元件, 它和Input,Select... 類似,但是它只是一個純展示控制元件
......你還可以自己封裝其他很多自定義的表單控制元件
實現
ZHForm:
/**
此元件接收的props:
form, (object) //必填, 執行Antd的Form.create() 之後 生成的form物件,ZHForm需要用它進行資料收集和校驗等
title (string), // 可選, 展示當前表單的標題
dataSource (object[]),// 資料來源,如果指定了dataSource 則預設從dataSource 取各欄位值, 結構:{name: 'Bob',hobby:'movie'}
fields (object[field]) : [ // 必填,根據它自動生成表單
// field結構:
{
key: 'template', // 必選, 用於react渲染唯一標識,將作為回傳資料的 key
type: 'Input' // 必填, 定義 表單控制元件 型別 (對映關係請看 getFormItem.js )
options // 可選,如果為單選/核取方塊組 或者 下拉選單時會需要它, 會傳遞給getFormItem.js 進行渲染, 結構: [{key: 'abc', label: '文字'}]
render: (value, dataSource) => {} // 可選,自行渲染
renderOnlyForItem: true or false // 可選,是否在 <Form.Item>值進行渲染, 與 render方法搭配使用,
formItemConfig: {}, // 參考 antdesign 中 Form.item 的 props
itemConfig: {}, // 參考 type 值對應元件的 props
fieldDecoratorConfig: {}, // 參考 antdesign Form 中 getFieldDecotor 的 第二個引數的配置(如果配置initialValue,則會忽略dataSource[key]的值)
showDivideLine: true // form下方是否展示分割線 預設true
},
]
*/
import React from 'react'
import PropTypes from 'prop-types'
import {Form} from 'antd'
import * as R from 'ramda'
import {FORM_ITEM_LAYOUT} from '../../constants/style'
import getFormItem from './../../utils/getFormItem'
import styles from './ZHForm.less'
export default class ZHForm extends React.PureComponent {
static Proptype = {
form: PropTypes.object.isRequired,
title: PropTypes.string,
dataSource: PropTypes.object,
fields: PropTypes.array.isRequired,
showDivideLine: PropTypes.bool,
}
static defaultProps = {
showDivideLine: true,
}
renderField = field => {
const {dataSource, form} = this.props
const {getFieldDecorator} = form
const {
key,
type,
options,
render,
renderOnlyForItem,
formItemConfig = {},
fieldDecoratorConfig = {},
itemConfig = {},
} = field
const initialValue = R.propEq(
'initialValue',
undefined,
fieldDecoratorConfig
)
? R.prop(key, dataSource)
: R.prop('initialValue', fieldDecoratorConfig)
const finalItemConfig = {type}
if (!R.isEmpty(itemConfig)) {
finalItemConfig.config = itemConfig
}
if (options) {
finalItemConfig.options = options
}
// 如果有render 則直接render
if (render && !renderOnlyForItem) {
return render(initialValue, dataSource)
}
return (
<Form.Item
className={styles.inputItem}
{...FORM_ITEM_LAYOUT}
key={key}
{...formItemConfig}
>
{render && renderOnlyForItem && render(initialValue, dataSource)}
{!renderOnlyForItem &&
getFieldDecorator(key, {
initialValue,
...fieldDecoratorConfig,
})(getFormItem(finalItemConfig))}
</Form.Item>
)
}
renderItems = () => {
const {fields = []} = this.props
return fields.map(field => {
return (
<React.Fragment key={field.key}>
{this.renderField(field)}
</React.Fragment>
)
})
}
render() {
const {title, showDivideLine} = this.props
return (
<React.Fragment>
{title && <div className={styles.formTitle}>{title}</div>}
{this.renderItems()}
{showDivideLine && <div className={styles.divideLine} />}
</React.Fragment>
)
}
}
複製程式碼
getFormItem.js 核心程式碼:
import TextPreview from '../components/Common/TextPreview'
// 此元件 負責: 接收 型別 和 options 返回一個 表單控制元件
const getFormItem = props => {
// props.type 控制元件型別
// props.options 可選 如果是 單選按鈕(組), 單選下拉框(組), 多選按鈕, 多選下拉框 則需要傳它;
// props.options 格式 [{key: 11, label: '我是label'}], 若type為 Radio/Checkbox 則 options陣列 長度為1
// props.config 傳遞給antd控制元件的 屬性
const {type = '', options, config = {}} = props
const renderOptions = optionType => {
return (
options &&
options.map(item => {
const {key, label} = item
switch (optionType) {
case 'select':
return (
<Select.Option key={key} value={key}>
{label}
</Select.Option>
)
.... // 其餘各種型別
}
})
)
}
let FieldItem
switch (type) {
case 'Preview':
FieldItem = <TextPreview {...config} />
break
case 'Input':
FieldItem = <Input style={defaultInputStyle} {...config} />
break
.... // 其餘各種型別
}
return FieldItem
}
export default getFormItem
複製程式碼
TextPreview元件
import React from 'react'
// 由於antd getFieldDecorator 方法內的自定義表單控制元件只能是個 class元件 故封裝
export default class TextPreview extends React.PureComponent {
render() {
const {value, ...restProps} = this.props
return (
<span {...restProps}>
{value}
</span>
)
}
}
複製程式碼
如何使用?
OK,我們開發完上面三個檔案之後,便可以痛快地開發業務程式碼了,我想立即開發一個編輯頁面的表單,該怎麼做呢?
核心程式碼:
// MyForm.js
// 獲取所有的表單的欄位
getCommonFields = () => {
const fields = [
{
type: 'InputNumber',
key: 'price',
formItemConfig: { // 此配置會傳遞給<Form.Item>
label: '金額(元)',
required: true,
},
{
type: 'InputNumber',
key: 'ratio',
formItemConfig: {
label: '配比',
required: true,
},
itemConfig: { // 此配置會傳遞給表單控制元件
min: 0,
max: 100,
precision: 2,
placeholder: '必填,最小 0,最大 100',
},
},
{
type: 'Preview', // 預覽模式, 如果是詳情頁,那每個欄位都用 Preview 模式即可
key: 'creator',
formItemConfig: {
label: '建立人',
},
},
]
return fields
}
render() {
// form: Form.create() 執行之後,此元件的props中會有form
const {data, form} = this.props
const allFields = this.getCommonFields()
return (
<ZHForm
dataSource={data}
fields={allFields}
form={form}
title="合同金額統計"
/>
)
}
複製程式碼
從程式碼中我們可以看到,只需要構造一個map形式的fields,然後傳入dataSource,即可生成表單!生成的表單如下圖:
你可能會有很多疑惑:(假設你在MyForm.js中使用ZHForm)
-
表單提交怎麼做? ZHForm只進行屬於資料展示、UI渲染, 提交資料在MyForm.js 進行
-
校驗怎麼做? 同上,你在MyForm.js 進行
form.validateFields
即可 -
如果有複雜資料 需要轉化後才能渲染到表單中, 怎麼做?
- 方法1: 自己先將data 轉化為表單接收的形式,再傳遞給dataSource
- 方法2: 用render函式,自行渲染控制元件
比如:
render: (value, dataSource) => { // 可以自行render你希望展示的UI和控制元件 const text = R.pathOr(0, ['order', 'netMoney'], dataSource) return <span>{money(text)}</span> }, 複製程式碼
-
如果有特殊形式的UI展示(比如輸入框後面有個別的元件) 或者控制元件之間有聯動關係怎麼做? 用render函式, 如果有聯動關係,可以在控制元件onChange回撥中執行 setFields方法
總結
至此,我們的React表單分析結束了。我的思路主要是以fields(表單欄位)的map為核心,寫一個元件去接管這些欄位並且渲染UI,之後每次開發新建、編輯、詳情頁面都可以複用一套map,感覺比重複地寫<Form.Item>....
省事很多。
關於如何渲染表單,上一篇文章基於React的表單開發的分析(上)
中有人給我評論,推薦使用可以和Antd無縫銜接的noform庫,我看了這個庫,它主要是將類似Antd的表單進行抽象,資料和檢視分開,優點是:它將表單控制元件封裝得更輕量,可以寫更少的程式碼,而且可以在新建和詳情頁複用程式碼。缺點是仍然需要自己去寫<Form.Item>
這樣的UI,而且生態還不夠好。
上週我們小組分享的時候同事推薦了一個react-json-schema, 感覺很強大,我的思路和它很像, 都是利用map形式的schema去渲染出我們想要的表單,大家也可以試試看看。