前面提到的,新增客户「基本信息」固定电话,账期,地址选择,这些封装成单独的组件,因为这类组件包含特有的数据或者逻辑
- 固定电话,区号-电话-分机号三个字段输入进行校验.
- 账期选择根据不同的类型,后面显示不同的提示.以及文本框的展示.
- 省市区下拉选择切换.切换省后要清掉后面的市和区的值.
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,
- });
- }
- },
- })
javascript
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;
- }
- // ...
- });
javascript
又因为还有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>
- );
- }
javascript
这样做,就仅仅是 「将代码拆分」,而不是「封装自定义表单组件」了。但这种做法带来的好处也是明显的,上面提到的第二个问题也同时解决了。
三、多余的字段处理
封装组件后,获取到的某些数据可能外面多包裹了一层对象,最后提交前,我们通过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
- });
- });
- };
javascript
虽然解决了这个问题,但我们需要在所有用到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
请在后台配置评论类型和相关的值。