前面提到的,新增客户「基本信息」固定电话,账期,地址选择,这些封装成单独的组件,因为这类组件包含特有的数据或者逻辑
- 固定电话,区号-电话-分机号三个字段输入进行校验.
- 账期选择根据不同的类型,后面显示不同的提示.以及文本框的展示.
- 省市区下拉选择切换.切换省后要清掉后面的市和区的值.
antd
官网中「自定义表单组件」就是一个有「特有逻辑」的组件,它同时包含两个字段。
一、 自定义表单代码分析:
以Demo简历基本信息为例来说明如何封装一个自定义表单组件:
import React from 'react';
import { Form, Input, DatePicker } from 'antd';
import SexSelect from '../SexSelect';
import EmailInput from '../EmailInput';
import CitySelect from '../CitySelect';
class BasicInfoForm extends React.PureComponent {
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className="basic__content">
<Form.Item label="姓名">
{getFieldDecorator('name', {
initialValue: 'pybyongbo',
rules: [
{
required: true,
message: '请输入姓名'
}
]
})(<Input placeholder="请输入姓名" />)}
</Form.Item>
<Form.Item label="出生年月">
{getFieldDecorator('birthday', {
rules: [
{
required: true,
message: '请选择出生年月'
}
]
})(<DatePicker placeholder="请选择出生年月" />)}
</Form.Item>
<Form.Item label="性别">
{getFieldDecorator('sex', {
initialValue: 1,
rules: [
{
required: true,
message: '请选择性别'
}
]
})(
<SexSelect />
)}
</Form.Item>
<Form.Item label="所在城市">
{getFieldDecorator('city', {
rules: [
{
required: true,
message: '请选择所在城市'
}
]
})(
<CitySelect />
)}
</Form.Item>
<Form.Item label="邮箱">
{getFieldDecorator('email', {
rules: [
{
required: true,
message: '请输入邮箱'
}
]
})(<EmailInput placeholder="请输入邮箱" />)}
</Form.Item>
</div>
);
}
}
export default Form.create({
// 当表单值发生改变时都会调用该方法
onValuesChange(props, changed, values) {
const { onChange } = props;
if (onChange) {
onChange({
...values,
...changed,
});
}
},
})(BasicInfoForm)
基本页面文件结构:
1.代码说明:
可以看到有constructor
、componentWillReceiveProps
和handleChange
,这三个方法都有各自的作用。
首先,handleChange
方法响应表单值的改变,并调用props.onChange
方法,实现了向父组件通信,将数据传递给父组件。
constructor
是为了配合initialValue
,当配置了initialValue
时,在constructor
中可以从props.value
上获取到对应值。
而componentWillReceiveProps
是为了配合resetFields
以及setFields
方法,能够从父组件直接控制表单的值,以及initialValue如果会发生改变,比如从接口中获取值,也是通过这里实现赋值的。
2.通过组合得到的自定义表单组件
性别选择和邮箱输入组件同理,有三层级目录.该组件是一个自定义表单组件
,实现了constructor
,componentWillReceiveProps
和handleChange
方法.这样的处理,存在5个表单,所以每个表单发生改变时,都会调用props.onChange
3.onValueChange 简化获取多个表单值
幸好借助antd的Form组件可以简化这部分代码.
Form.create({
// 当表单值发生改变时都会调用该方法
onValuesChange(props, changed, values) {
const { onChange } = props;
if (onChange) {
onChange({
...values,
...changed,
});
}
},
})
4.组合组件带来的问题和解决方法
OK,能满足我们获取值
的需求,但是存在两个问题:
- 丢失了校验规则
- 获取到的是外层对象basic字段,我们需要的是basic字段的值
二、恢复丢失的校验规则
如果有实际试用过该代码的人可能会有疑问,输入邮箱时会对输入内容进行校验啊,为什么说「丢失了校验规则」呢? 实际上即使邮箱格式不正确并且有错误提示,但点击「保存」后还是可以获取表单值,而开始的例子是不能的,并且会将页面滚动到邮箱输入处。
最直观的感受是什么都不填,直接点击「保存」按钮,最开始的实现 是可以正确校验的,而 拆分为自定义表单组件 后,点击按钮会通过校验,直接打印出当前的表单值。
1、自定义校验规则
参考antd
中的自定义表单,如果需要对自定义表单进行校验,需要通过自定义validator
实现
直接点击「保存」按钮后,发现虽然没有直接打印表单值,但页面上也没有显示错误信息,只有控制台显示async-validator
: ["请输入基本信息"]
,这说明校验规则的确生效了。
这是因为错误提示是由Form.Item显示的,必须将BasicInfoForm放在Form.Item组件内才会显示我们在callback传入的错误信息。
但是给BasicInfoForm
包裹Form.Item
后,虽然错误信息显示,但只会出现在最下方,无法实现在实际错误的表单下方显示,并且明显校验规则还需要我们再实现一次。
这也是一个Form.Item组件内无法同时存在两个及以上getFieldDecorator的原因。
2、更友好的错误展示
这两个缺点都是非常不友好的,如果希望使用Form.Item提供的错误展示机制,正确地在表单下方展示,要怎么做呢?
想到最开始的实现代码,虽然不怎么优雅,但校验却实实在在有用,能否直接复用呢?所以问题就是,为什么这样封装一层,原先的校验规则就失效了呢?
3、props.form 存储表单值
这是因为props.form
的问题。
props.form
简单来说就是一个store
,存储着所有经过props.form.getFieldDecorator
包装后的表单组件的值与校验规则。通过调用props.form.validateFieldsAndScroll
就可以对值进行校验了。
而我们的代码中,实际上存在多个props.form
,App
组件有一个,BasicInfoForm
组件也有一个,各自为政,互不干扰。
所有如果想校验BasicInfoForm
组件的表单,就必须用该组件内的form.validateFieldsAndScroll
。
第一反应是使用ref
,但由于BasicInfoForm
是被getFieldDecorator
装饰后的组件,props
上并没有我们期望的form
属性。这时应该使用官方提供的wrappedComponentRef
替代。
this.basicInfoForm.props.form.validateFieldsAndScroll((err, values) => {
if (err) {
return;
}
// ...
});
又因为还有workExpForm和projectExpForm,所以就要再获取这两个表单的值,再组合起来。
4、自定义表单组件带来更多问题?
看到这里,就会有疑问啦,拆分后带来一大堆问题.难道不应该对组件进行拆分吗?
如果只将一些简单的组件作为自定义表单组件,比如CitySelect,其他的保持原样是不是更简单些? 这也不失为一种方法,所以是否应该拆分,就是仁者见仁智者见智了.
但是就上面的问题而言,有一种解决办法,就是只有一个props.form
,即只在App
组件使用Form.create
包装,其他组件都通过props
传递form
.所以代码会这样:
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className="resume">
<ResumeForm form={this.props.form} />
<Button type="primary" onClick={this.save}>保存</Button>
<Button onClick={this.reset}>重置</Button>
</div>
);
}
这样做,就仅仅是 「将代码拆分」,而不是「封装自定义表单组件」了。但这种做法带来的好处也是明显的,上面提到的第二个问题也同时解决了。
三、多余的字段处理
封装组件后,获取到的某些数据可能外面多包裹了一层对象,最后提交前,我们通过values进行拿到所有的值,进行解构下,然后组装一下就可以啦.如下:
save = () => {
console.log(this.basicInfoForm.props);
this.basicInfoForm.props.form.validateFieldsAndScroll((err, values) => {
if (err) {
return;
}
const body = JSON.stringify(values, null, 2);
console.log({
...body.basic,
work: body.work,
projects: body.projects
});
});
};
虽然解决了这个问题,但我们需要在所有用到ResumeForm组件的地方处理数据
,这明显不够优雅.能否做到获取的values就是我们期望的最终数据
呢?
从我们的使用经验来说,获取到的数据是和getFieldDecorator强相关的,key是参数,value是表单的值。所以应该从getFieldDecorator入手。
1、表单值转换
例如我们也可以直接在组件里面将基本信息的值进行转换,提交的时候就不用进行解构啦.然后删除属性操作啦.使用normalize
方法即可.该方法是用来「转换默认的 value」给控件。
<div className="basic__content">
<Form.Item>
{getFieldDecorator("basic", {
// rules: [{
// validator: this.checkBasicInfo
// }],
normalize: function(value) {
return {...value}
}
})(<BasicInfoForm wrappedComponentRef={(basicInfo) => this.basicInfoForm = basicInfo}/>)}
</Form.Item>
</div>
四、表单默认值
默认值也是表单组件一个非常重要的功能,无论是初始化默认值,减少用户填写成本;还是进入编辑状态时赋值,都要用到该功能。
对表单字段进行初始化默认赋值,使用initialValue
.
自定义表单实现 initialValue 默认值.因为当initialValue发生改变时,会调用组件的componentWillReceiveProps,并将initialValue作为props.value传入,实现了默认值的效果。
进行编辑表单操作的时候,都需要对表单进行初始化赋值.我们之前抽离的公共组件,如固定电话,地址下拉选择.都需要默认的赋值一个对象.例如公司地址组件:
<FormItem label="公司地址">
{getFieldDecorator('companyAddress', {
initialValue:customerbasicInfo && customerbasicInfo.id
? {
officeCountry: customerbasicInfo.officeCountry || '',
officeCountryName: customerbasicInfo.officeCountryName || '',
officeProvince: customerbasicInfo.officeProvince || '',
officeProvinceName: customerbasicInfo.officeProvinceName || '',
officeCity: customerbasicInfo.officeCity || '',
officeCityName: customerbasicInfo.officeCityName || '',
officeArea: customerbasicInfo.officeArea || '',
officeAreaName: customerbasicInfo.officeAreaName || ''
}: '',
rules: []
})(<AddressSelect />)}
</FormItem>
对初始赋值进行判断,是新增操作还是编辑操作~
Comments
请在后台配置评论类型和相关的值。