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