阅读 1064

基于React的表单开发的分析(下)

背景

上周我写了一篇文章: 基于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去渲染出我们想要的表单,大家也可以试试看看。

相关链接

关注下面的标签,发现更多相似文章
评论