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

giovanni發表於2018-08-12

背景

上週我寫了一篇文章: 基於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去渲染出我們想要的表單,大家也可以試試看看。

相關連結

相關文章