1、基本概述
项目开发过程中,表单组件是最常用的组件之一,应用场景也多见于列表搜索、简单的新增和编辑弹窗、复杂的新增和编辑页面。Ant design的组件库中Form表单也提供了对应的API可供开发使用,极大的提高的开发速度。
在单独的页面里,我们通常引入Form组件及其对应的表单项组件(例如Input、Select、Checkbox等等)。由于通过Form.create()去处理当前组件,所以在this.props中能拿到form对应的实例,就可以使用getFieldDecorator等方法。具体的做法如下:
import React, {Component} from 'react'
import {connect} from 'dva'
import {Link} from 'dva/router';
import {Input, Radio, Row, Select} from 'antd';
class Example extends Component {
//......
render(){
const {example, form} = this.props
const {getFieldDecorator} = form;
//......
return ({
<div>
<Form>
<Row gutter={16}>
<Col span={6}>
<FormItem {...formItemLayout} label="发货人" hasFeedback>
{getFieldDecorator('senderName', {
initialValue: example.senderName || "",
})(
<Input placeholder="请输入发货人" maxLength="20"/>
)}
</FormItem>
</Col>
<Col span={6}>
<FormItem {...formItemLayout} label="联系方式" hasFeedback >
{getFieldDecorator('senderMobile', {
initialValue: example.senderMobile || "",
rules: [{
required: true, message: '请输入联系方式',
}],
})(
<Input placeholder="请输入联系方式"/>
)}
</FormItem>
</Col>
//......
</Row>
</Form>
</div>
})
}
}
export default connect(({example}) => ({example}))(Form.create()(Example))
在render方法中,我们会编写多个FormItem,并给每个FormItem设置label、name、initialValue、rules等等一些配置。当表单内容较多时,编写上述表单,就会显得很冗余,会存在一些重复的代码。
抽象来看,其实每个表单项都可以分为两部分:
(1)、 FormItem组件:对于FormItem的组件,具体属性可以参考Form组件的API,通常会用到的一些属性就是label、formItemLayout(布局);
(2)、 FormItem的子组件: 对于FormItem的组件,每个子组件对应的属性可以参考各自组件的API,因为都是通过getFieldDecorator去处理,因此都含有共同的属性name、initialValue、rules。
因次产生了一个想法:通过循环遍历一个表单配置列表List,表单配置列表中的每一项包含两部分:一部分是对FormItem组件的配置,一部分是对FormItem的子组件的配置。那上述表单内容就完全可以通过实现一个list的map遍历就可以解决。更进一步,把以上这些操作封装成一个组件,接受一个配置列表的props,然后在业务代码中就可以直接引用该组件,并传递具体的表单配置列表给对应的props,就应该可以生成一个完整的表单。
优点:
(1)、通过以上配置生成表单组件,由于view层都集成到组件中,无需关注View层的代码,可以提交开发效率;
(2)、更利于代码维护,page页面更专注于逻辑处理,配置页面更关注于view层交互。
2、具体实现
实现源码请参考:https://github.com/zhengchangshun/myUmi/tree/master/src/components/business/GenerateForm
2.1 、常用的子组件的类型
常用的子组件类型如下,mapTypeToComponent定义了的组件的数据结构关系,可配置表单组件的配置列表会根据该数据结构的映射关系构造一个具体的FormItem。其中:
WrappedComponent:用来指定具体的使用哪个子组件,
defaultProps:用来设置子组件的默认属性 - 针对ant design中一些非默认属性,但是项目中的可以统一的,例如时间相关插件的locale可以根据项目统一设置成中文时区;
style:代表该组件的一些默认样式,例如RadioGroup中的Radio增加间距marginLeft: ‘10px’。
select、checkboxgroup、radiogroup这三种类型的组件需要额外的两个属性:子组件类型和子组件数据源,其中SubComponent代表子组件类型,例如Select对应的Option,RadioGroup对应的Radio;optionsData用来指定选择项option对应的数据来源。
说明:只要能被this.props.form.getFieldDecorator操作的组件,都可以添加到该组件的支持表单类型中。
import {Form, Select, DatePicker, Row, Col, Input, InputNumber, Checkbox, Radio} from 'antd';
import locale from 'antd/lib/date-picker/locale/zh_CN';
const FormItem = Form.Item;
const {Option} = Select;
const {TextArea, Password} = Input;
const RadioGroup = Radio.Group;
const CheckboxGroup = Checkbox.Group
const {RangePicker, MonthPicker} = DatePicker;
/*目前支持的form表单类型*/
const mapTypeToComponent = {
'label': '',
'input': {
WrappedComponent: Input,
},
'inputnumber': {
WrappedComponent: InputNumber,
},
'password': {
WrappedComponent: Password,
},
'select': {
WrappedComponent: Select,
defaultProps: {allowClear: true},
optionsData: 'selectOptions',
SubComponent: Option
},
'timepickerrange': {
WrappedComponent: TimePickerRange
},
'datepicker': {
WrappedComponent: DatePicker,
defaultProps: {locale}
},
'monthpicker': {
WrappedComponent: MonthPicker,
defaultProps: {locale}
},
'rangepicker': {
WrappedComponent: RangePicker,
defaultProps: {locale}
},
'checkbox': {
WrappedComponent: Checkbox,
},
'checkboxgroup': {
WrappedComponent: CheckboxGroup,
optionsData: 'checkboxOptions',
SubComponent: Checkbox,
style: {
marginLeft: '10px'
}
},
'textarea': {
WrappedComponent: TextArea,
},
'radiogroup': {
WrappedComponent: RadioGroup,
optionsData: 'radioOptions',
SubComponent: Radio,
style: {
marginLeft: '10px'
}
},
}
2.2 、核心代码的实现
封装的可配置表单组件GenerateForm,先撸代码,在做分析,具体代码如下:
class GenerateForm extends React.Component {
//提供给父组件用的校验方法
verify = callback => {
this.props.form.validateFields((errors, fieldsValue) => {
callback && callback(errors, fieldsValue);
});
};
/*form实例*/
getForm = () => {
return this.props.form
}
render() {
/*formSet代表form表单的配置*/
const {className = '', formSet, form} = this.props;
const {getFieldDecorator} = form;
return (
<Form className={`${styles.generateFormWrap} ${className}`}>
<Row>
{
formSet.map((item, key) => {
const {
isShow = true, //该配置项是否显示
rules, //ant design Form原生校验规则
initialValue, //ant design Form原生初始值
validate = [], //ant design Form原生校验属性
type, //组件类型
label, //ant design Form原生表单项label
colon = true, //ant design Form原生:是否显示label后边的冒号
props, //外部传入给组件的属性
name, //ant design Form原生name属性
span = 8, //表单项的布局长度
formItemLayout = {labelCol: {span: 8}, wrapperCol: {span: 16}} //表单项lable、表单组件的布局
} = item,
{WrappedComponent, defaultProps} = mapTypeToComponent[type.toLowerCase()],
options = {
rules,
validate
};
if ('initialValue' in item) {
options.initialValue = initialValue;
}
/*控制编辑、新增时,部分选型是否显示*/
if (!isShow) {
return null
}
/*select 、radiogroup、checkboxgroup等含有子项的组件 */
if (type.toLowerCase() === 'select' || type.toLowerCase() === 'radiogroup' || type.toLowerCase() === 'checkboxgroup') {
const {optionsData, SubComponent, style} = mapTypeToComponent[type.toLowerCase()]
const subOptionsData = item[optionsData]
const models = item.models; //option项的映射关系,表示value和label的取值字段
const [vauleKey = 'value', labelKey = 'label'] = models || [];
return (
<Col span={span} key={key}>
<FormItem label={label} colon={colon} {...formItemLayout} >
{getFieldDecorator(name, options)(
<WrappedComponent {...defaultProps} {...props}>
{
subOptionsData.length > 0 && subOptionsData.map((v, i) => {
return <SubComponent key={i} value={v[vauleKey]} style={style}>{v[labelKey]}</SubComponent>
})
}
</WrappedComponent>
)}
{
item.addonAfter && item.addonAfter
}
</FormItem>
</Col>
);
}
/*文本*/
if (type.toLowerCase() === 'label') {
return (
<Col span={span} key={key}>
<FormItem label={label} colon={colon} {...formItemLayout}>
<span>{initialValue}</span>
{
item.addonAfter && <span style={{marginLeft: '5px'}}>{item.addonAfter}</span>
}
</FormItem>
</Col>
)
}
return (
<Col span={span} key={key}>
<FormItem label={label} colon={colon} {...formItemLayout} >
{getFieldDecorator(name, options)(
<WrappedComponent {...defaultProps} {...props} form={form}></WrappedComponent>
)}
{
item.addonAfter && item.addonAfter
}
</FormItem>
</Col>
)
})
}
</Row>
</Form>
)
}
}
GenerateForm.propTypes = {
formSet: PropTypes.array, //表单配置项
className: PropTypes.string, //外部传入的class
}
export default Form.create()(GenerateForm);
2.3 、核心代码的阐述
首先,来分析组件的props属性,通过propTypes配置可以看到,GenerateForm组件可以接受两个属性formSet,className。其中formSet就是对应的表单配置项,是一个数组结构,通过第1部分的分析,数据项应该包含FormItem的配置以及子组件对于的配置;className用来接受外部的样式。
其次,来分析一下render方法。通过循环遍历formSet,其目的就是为了解析每一个单独的表单项的配置item,从而生成对应的表单元素。其中item包含属性的配置,具体的释义可见后边的注释。几个重要的属性,单独解释一下
:
type值配合上文中的mapTypeToComponent 来确定是用哪种子组件;
props外部配置文件设置的子组件的一些属性和事件Handel,具体的设置可参考ant design中该类型组件的API文档;
rules、validate、initialValue通过处理之后,作为getFieldDecorator的options属性;
isShow是为了确定该表单项是否需要渲染,一般用来解决添加、编辑等表单中表单元素的差异性;
models是一个数组[],应用在select、checkboxgroup、radiogroup的配置项中,通常选择项的option取值对应数据结构中的[‘value’,‘label’],但是实际的数据结构可能不是这两个字段,models主要是为了解决这个问题,其中第一项告诉option组件value应该去数据项的那个字段,第二项告诉option组件label应该去数据项的那个字段(参考elementUI的select组件);
addonAfter可以是文本,也可以是组件,类似于ant design Input组件addonAfter属性,附加在表单项元素之后的内容;
optionsData也是应用在select、checkboxgroup、radiogroup的配置项中,配合mapTypeToComponent来确定,option选型取配置数据中的那个字段。例如select类型的组件,通过查询mapTypeToComponent可知应该使用item中的selectOptions这个属性来获取对应数据。
render方法分三种情况去处理,一类是有option子项的select、checkboxgroup、radiogroup;一类是纯文本类型;最后一类是其他类型的表单元素。本质上都是通过FormItem包裹子组件,并设置FormItem的属性,然后通过getFieldDecorator去处理,并设置name、options属性,最后的是子组件的处理,包括默认属性defaultProps,以及外部设置的props属性的设置,部分子组件可能需要传递form实例。针对select、checkboxgroup、radiogroup类型的组件,对subOptionsData做遍历生成对应的option选型,同时根据models的映射关系做相应数据处理。
最后,GenerateForm提供两个方法:一个是表单校验,表单校验成功后,将表单项内容丢给callback处理;另外一个是对外暴露form类型的实例,在父组件中可以通过ref获取当前form实例,可以自行调用form的方法对表单进行处理,例如getFieldsValue、setFieldsValue等form的原生方法。
3、实现案例。
通过GenerateForm生成如下弹窗,由于GenerateForm已经封装了view,因此我们只需要重点关注配置项。
具体的配置项如下:
export function modalSet(_self) {
return [
{
type: 'input',
name: 'partyName',
label: '会员名',
initialValue: _self.props.userModal.partyName,
props: {
disabled: _self.props.userModal.isEdit,
},
rules: [
{required: true, message: '请输入会员名'},
{max: 20, message: '字数为6-20个字'},
{min: 6, message: '字数为6-20个字'}
]
},
{
type: 'input',
name: 'name',
label: '姓名',
initialValue: _self.props.userModal.name,
rules: [
{required: true, message: '请输入姓名'},
{max: 20, message: '字数不能超过20个字'}
]
},
{
type: 'input',
name: 'phone',
label: '手机号',
initialValue: _self.props.userModal.phone,
rules: [
{required: true, message: '请输入手机号'},
{pattern: patternPhone, message: '手机号格式不正确'}
]
},
{
type: 'checkboxgroup',
name: 'roles',
label: '角色名称',
span: 24,
formItemLayout: {
labelCol: {span: 4}, wrapperCol: {span: 20}
},
initialValue: _self.props.userModal.roles,
models: ['id', 'roleName'], // 对应option中value,label取值的key
checkboxOptions: _self.props.userList.roleList
},
{
type: 'Password',
name: 'password',
label: '密码',
initialValue: _self.props.userModal.password,
rules: [
{required: true, message: '请输入密码'},
{max: 20, message: '字数不能超过20个字'}
]
},
{
type: 'Password',
name: 'confirmPassword',
label: '确认密码',
initialValue: _self.props.userModal.confirmPassword,
rules: [
{validator: _self.comfirmPassword}
]
},
{
type: 'textarea',
name: 'remark',
label: '备注',
span: 24,
formItemLayout: {
labelCol: {span: 4}, wrapperCol: {span: 20}
},
props: {
autosize: {minRows: 3, maxRows: 6}
},
initialValue: _self.props.userModal.remark,
rules: [
{max: 200, message: '字数不能超过200个字'}
],
},
{
type: 'radiogroup',
name: 'status',
label: '分公司是否有效',
isShow: _self.props.userModal.isEdit, /*编辑时显示*/
span: 24,
formItemLayout: {
labelCol: {span: 4}, wrapperCol: {span: 20}
},
initialValue: _self.props.userModal.status,
radioOptions: [
{value: '启用', label: '启用'},
{value: '禁用', label: '禁用'}
],
rules: [
{required: true, message: '请选择分公司是否有效'},
],
}
]
}
在父组件的render方法中通过调用modalSet方法,并传入this对象。则对应的配置项中,就能够获取model里边的数据。如果配置项中的元素有事件处理,也可以在props中添加事件类型,事件方法可以通过this指向父组件中的实例方法。具体的实例可以参考代码:https://github.com/zhengchangshun/myUmi/tree/master/src/pages/systemManage/user/list。 其中GenerateModal组件是通过GenerateForm生成的弹窗。
上一篇:基于Umi搭建的个人Dva脚手架(三) - 多Layout设计
下一篇:基于Umi搭建的个人Dva脚手架(五) - 可配置的搜索、弹窗组件封装