前面提到,「基本信息」包含姓名、出生年月、性别、城市以及邮箱共 5 个输入项。而其中性别、省市与邮箱封装为了单独的组件,因为这类组件包含特有的数据或者逻辑。
- 性别选择,因为有性别对应的 value。当然也可以将性别数据作为常量引入。
- 省市选择,包含了省市信息。
- 邮箱,有补全邮箱后缀等逻辑。
antd
官网中「自定义表单组件」就是一个有「特有逻辑」的组件,它同时包含两个字段。
一、自定义表单标准代码分析
以省市选择为例来说明如何封装一个自定义表单组件。
1 | /** |
1、代码说明
这段代码属于标准的「antd 自定义表单」,既可作为普通表单使用,也可配合antd
中的Form
组件使用。
可以看到有constructor
、componentWillReceiveProps
和handleChange
,这三个方法都有各自的作用。
首先,handleChange
方法响应表单值的改变,并调用props.onChange
方法,实现了向父组件通信,将数据传递给父组件。
constructor
是为了配合initialValue
,当配置了initialValue
时,在constructor
中可以从props.value
上获取到对应值。
而componentWillReceiveProps
是为了配合resetFields
以及setFields
方法,能够从父组件直接控制表单的值,以及initialValue
如果会发生改变,比如从接口中获取值,也是通过这里实现赋值的。
2、通过组合得到的自定义表单组件
性别选择和邮箱输入组件同理,所以我们的BasicInfoForm
代码应该如下:
1 | /** |
虽然实现了 UI,但这并不是一个「表单组件」,如果希望该组件是一个「自定义表单组件」,应该和上面的省市选择一样,实现constructor
、componentWillReceiveProps
和handleChange
方法,前两个好说,问题就在于handleChange
方法,由于存在 5 个表单,所以需要每个表单发生改变时,都调用props.onChange
,那就需要有
- handleNameChange
- handleBirthdayChange
- handleSexChange
- handleCityChange
- handleEmailChange
3、onValueChange 简化获取多个表单值
幸好借助antd
的Form
组件可以简化这部分代码,
1 | @Form.create({ |
将组件替换掉我们页面组件中「基本信息」相关的代码,这是线上示例「 封装基本信息表单」。
这里是我们实际输入后能够获取到的数据
1 | { |
4、组合组件后带来的问题
OK,能满足我们获取值的需求,但存在两个问题
- 1、丢失了校验规则
- 2、获取到的是
basic
字段,我们需要的是basic
字段的值。
二、恢复丢失的校验规则
如果有实际试用过该代码的人可能会有疑问,输入邮箱时会对输入内容进行校验啊,为什么说「丢失了校验规则」呢?
实际上即使邮箱格式不正确并且有错误提示,但点击「保存」后还是可以获取表单值,而开始的例子是不能的,并且会将页面滚动到邮箱输入处。
最直观的感受是什么都不填,直接点击「保存」按钮,最开始的实现 是可以正确校验的,而 拆分为自定义表单组件 后,点击按钮会通过校验,直接打印出当前的表单值。
1、自定义校验规则
参考antd
中的自定义表单,如果需要对自定义表单进行校验,需要通过自定义validator
实现,代码如下:
1 | import React from 'react'; |
直接点击「保存」按钮后,发现虽然没有直接打印表单值,但页面上也没有显示错误信息,只有控制台显示async-validator: ["请输入基本信息"]
,这说明校验规则的确生效了。
这是因为错误提示是由Form.Item
显示的,必须将BasicInfoForm
放在Form.Item
组件内才会显示我们在callback
传入的错误信息。
但是给BasicInfoForm
包裹Form.Item
后,虽然错误信息显示,但只会出现在最下方,无法实现在实际错误的表单下方显示,并且明显校验规则还需要我们再实现一次。线上实例
这也是一个
Form.Item
组件内无法同时存在两个及以上getFieldDecorator
的原因。
2、更友好的错误展示
这两个缺点都是非常不友好的,如果希望使用Form.Item
提供的错误展示机制,正确地在表单下方展示,要怎么做呢?
想到最开始的实现代码,虽然不怎么优雅,但校验却实实在在有用,能否直接复用呢?所以问题就是,为什么这样封装一层,原先的校验规则就失效了呢?
1 | @Form.create({ |
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
替代。
1 | this.basicInfoForm.props.form.validateFieldsAndScroll((err, values) => { |
又因为还有workExpForm
和projectExpForm
,所以就要再获取这两个表单的值,再组合起来。
4、自定义表单组件带来更多问题?
看到这,就会有疑问了,拆分后带来了一大堆的问题。难道不应该对组件进行拆分吗?
如果只将一些简单的组件作为自定义表单组件,比如CitySelect
,其他的保持原样是不是更简单些?。
这也不失为一种方法,所以是否应该拆分,就是仁者见仁智者见智了。
但是就上面的问题而言,有一种解决办法,就是只有一个props.form
,即只在App
组件使用Form.create
包装,其他组件都通过props
传递form
。所以代码会变成这样:
1 | render() { |
这样做,就仅仅是「将代码拆分」,而不是「封装自定义表单组件」了。但这种做法带来的好处也是明显的,上面提到的第二个问题也同时解决了。
三、多余的字段
再来详细谈谈第二个问题。
1 | { |
封装组件后,获取到的是这样的数据,而我们实际需要的是
1 | { |
最后提交前处理一下就好了嘛,就这样:
1 | save = () => { |
虽然解决了这个问题,但我们需要在所有用到ResumeForm
组件的地方处理数据,这很明显不够优雅。能否做到获取到的values
就是我们期望的最终数据呢?
从我们的使用经验来说,获取到的数据是和getFieldDecorator
强相关的,key
是参数,value
是表单的值。所以应该从getFieldDecorator
入手。
1、表单值转换
还有一个类似的问题,当组件使用到「日期输入」时,往往需要将表单的值转换为时间戳,这也是重复工作,能否表单暴露的值就是时间戳呢?
如果上面的city
数据,我们只需要最后一位,这个问题似乎是一样的。但这个问题可以使用normalize
解决,该方法是用来「转换默认的 value」给控件。
就是可以将表单的值做处理,但要求处理后的值也是控件能够接受的。默认我们选择城市后得到["33", "3301", "330105"]
,可以将其转换为["330105]
,但不能变成"330105"
,所以无法处理moment
变成时间戳。
1 | <Form.Item label="所在城市"> |
四、默认值
默认值也是表单组件一个非常重要的功能,无论是初始化默认值,减少用户填写成本;还是进入编辑状态时赋值,都要用到该功能。
1、默认值通过接口得到不会生效
antd
的表单组件,都提供了defaultValue
属性,用以配置默认值:
1 | class App extends React.Component { |
渲染后可以看到内表单有wuya
默认值。但如果默认值是从接口请求得到的,就无法达到我们的预期效果。
1 | class App extends React.Component { |
而如果改成initialValue
就能够生效。
1 | @Form.create() |
2、自定义表单实现 initialValue 默认值
前面我们自己实现了CitySelect
,它支持默认值吗?
1 | @Form.create() |
幸运的是支持。因为当initialValue
发生改变时,会调用CitySelect
的componentWillReceiveProps
,并将initialValue
作为props.value
传入,实现了默认值的效果。
3、支持 defaultValue 默认值
那CitySelect
支持defaultValue
默认值吗?很明显不支持对吧,因为回头看我们的CitySelect
组件代码,完全没有出现过defaultValue
。
1 | @Form.create() |
即使是直接给初始值也不行,更别说通过接口获取默认值了。那么接下来在不影响原有功能的基础上,添加defaultValue
的支持。
1 | // CitySelect.js |