基於React的表單開發的分析(下)

NO IMAGE

背景

上週我寫了一篇文章: 基於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,即可生成表單!生成的表單如下圖:

基於React的表單開發的分析(下)

你可能會有很多疑惑:

(假設你在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去渲染出我們想要的表單,大家也可以試試看看。

相關鏈接

相關文章

UC面試總結

iOS動態化熱修復方案

GoPlay原理詳解

前端面試總結