12月05, 2019

React+Antd 自定义表单问题-2

前面提到的,新增客户「基本信息」固定电话,账期,地址选择,这些封装成单独的组件,因为这类组件包含特有的数据或者逻辑

  • 固定电话,区号-电话-分机号三个字段输入进行校验.
  • 账期选择根据不同的类型,后面显示不同的提示.以及文本框的展示.
  • 省市区下拉选择切换.切换省后要清掉后面的市和区的值.

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.代码说明:

可以看到有constructorcomponentWillReceivePropshandleChange,这三个方法都有各自的作用。 首先,handleChange方法响应表单值的改变,并调用props.onChange方法,实现了向父组件通信,将数据传递给父组件。

constructor是为了配合initialValue,当配置了initialValue时,在constructor中可以从props.value上获取到对应值。

componentWillReceiveProps是为了配合resetFields以及setFields方法,能够从父组件直接控制表单的值,以及initialValue如果会发生改变,比如从接口中获取值,也是通过这里实现赋值的。

2.通过组合得到的自定义表单组件

性别选择和邮箱输入组件同理,有三层级目录.该组件是一个自定义表单组件,实现了constructor,componentWillReceivePropshandleChange方法.这样的处理,存在5个表单,所以每个表单发生改变时,都会调用props.onChange

3.onValueChange 简化获取多个表单值

幸好借助antd的Form组件可以简化这部分代码.

Form.create({
    // 当表单值发生改变时都会调用该方法
  onValuesChange(props, changed, values) {
    const { onChange } = props;
    if (onChange) {
      onChange({
        ...values,
        ...changed,
      });
    }
  },
})

4.组合组件带来的问题和解决方法

OK,能满足我们获取值的需求,但是存在两个问题:

  1. 丢失了校验规则
  2. 获取到的是外层对象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.formApp组件有一个,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>

对初始赋值进行判断,是新增操作还是编辑操作~

本文链接:https://901web.com/post/Antd自定义表单问题2.html

-- EOF --

Comments

请在后台配置评论类型和相关的值。