1. 前言
很久没更新博客了,皮的嘛,就不谈了,不过问题不大,今天就结合 项目中写的一个 React 高阶组件 的实例 再来讲一讲,结合上一篇文章,加深一下印象
2. Ant Design 的 Form 组件
国民组件库 Ant-Design 的 Form 库 想必大家都用过,比较强大,基于 rc-form 封装,功能比较齐全
最近项目中遇到了一个需求,普通的一个表单,表单字段没有 填完的时候,提交按钮 是 disabled 状态的,听起来很简单,由于用的是 antd 翻了翻文档,copy 了一下代码 , 发现需要些不少的代码
1 | import { Form, Icon, Input, Button } from 'antd'; |
3. 那么问题来了
上面的代码咋一看没什么毛病,给每个字段绑定一个 validateStatus 去看当前字段 有没有触碰过 并且没有错,并在 组件渲染的时候 触发一次验证,通过这种方式 来达到 disabled 按钮的目的,但是要命的 只是 实现一个 disabled 的效果,多写了这么多的代码,实际遇到的场景是 有 10 多个这种需求的表单,有没有什么办法不写这么多的模板代码呢?于是我想到了 高阶组件
4. 开始干活
由于 Form.create() 后 会给 this.props 添加 form 属性 , 从而使用它提供的 api, 经过观察 我们预期想要的效果有以下几点
1 | // 使用效果 |
要达到如下效果
- 1.
componentDidMount的时候 触发一次 字段验证 - 这时候会出现错误信息,这时候需要干掉错误信息
- 然后遍历当前组件所有的字段,判断 是否有错
- 提供一个
this.props.hasError类似的字段给当前组件。控制 按钮的disabled状态
- 提供一个
- 支持非必填字段,(igonre)
- 支持编辑模式 (有默认值)
5. 实现 autoBindForm
1 | import * as React from 'react' |
首先 Form.create 一下我们需要包裹的组件,这样就不用每一个页面都要 create 一次
然后我们通过 antd 提供的 wrappedComponentRef 拿到了 form 的引用
根据 antd 的文档 , 我们要实现想要的效果,需要用到 如下 api
validateFields验证字段getFieldsValue获取字段的值setFields设置字段的值getFieldsError获取字段的错误信息isFieldTouched获取字段是否触碰过
1 | class AutoBindForm extends WrappedComponent |
继承我们需要包裹的组件(也就是所谓的反向继承), 我们可以 在初始化的时候 验证字段
1 | componentDidMount(){ |
由于进入页面时 用户并没有输入,所以需要手动清空 错误信息
1 | componentDidMount() { |
通过 getFieldsValue() 我们可以动态的拿到当前 表单 所有的字段,然后再使用 setFields 遍历一下 把所有字段的 错误状态设为 null, 这样我们就实现了 1,2 的效果,
6. 实现实时的错误判断 hasError
由于子组件 需要一个 状态 来知道 当前的表单是否有错误,所以我们定义一个 hasError 的值 来实现,由于要是实时的,所以不难想到用 getter 来实现,
熟悉Vue 的同学 可能会想到 Object.definedPropty 实现的 计算属性,
本质上 Antd 提供的 表单字段收集也是通过 setState, 回触发页面渲染,在当前场景下,直接使用 es6 支持的get 属性即可实现同样的效果代码如下
1 |
|
代码很简单 , 在每次 getter 触发的时候,我们用 some 函数 去判断一下 当前的表单是否触碰过 或者有错误,在创建表单这个场景下,如果没有触碰过,一定是没输入,所以不必验证是否有错
最后 在 render 的时候 将 hasError 传给 子组件
1 | render() { |
同时我们定义下 type
1 | export interface IAutoBindFormHelpProps { |
写到这里,创建表单的场景,基本上可以用这个高阶组件轻松搞定,但是有一些表单有一些非必填项,这时就会出现,非必填项但是认为有错误的清空,接下来,改进一下代码
7. 优化组件,支持 非必填字段
非必填字段,即认为是一个配置项,由调用者告诉我哪些是 非必填项,当时我本来想搞成 自动去查找 当前组件哪些字段不是 requried 的,但是 antd 的文档貌似 莫得,就放弃了
首先修改函数,增加一层柯里化
1 | export default (filterFields: string[] = []) => |
1 | @autoBindForm(['fieldA','fieldB']) //需要实现的组件 |
修改 hasError 的逻辑
1 | get hasError() { |
逻辑很简单粗暴,遍历一下需要过滤的字段,看它有没有触碰过,如果触碰过,就不加入错误验证
同理,在 初始化的时候也过滤一下,
首先通过 Object.keys(getFieldsValue) 拿到当前表单 的所有字段,由于 这时候不知道哪些字段 是 requierd 的,机智的我
validateFields 验证一下当前表单,这个函数 返回当前表单的错误值,非必填的字段 此时不会有错误,所以 只需要拿到当前错误信息,和 所有字段 比较 两者 不同的值,使用 loadsh 的 xor 函数 完成
1 | const filterFields = xor(fields, Object.keys(err || [])); |
最后清空 所有错误信息
完整代码:
1 | componentDidMount() { |
经过这样一波修改,支持非必填字段的需求就算完成了
8. 最后一波,支持默认字段
其实这个很简单,就是看子组件是否有默认值 , 如果有 setFieldsValue 一下就搞定了,子组件和父组件约定一个 defaultFieldsValue
完整代码如下
1 | import * as React from 'react'; |
这样一来,如果子组件 有 defaultFieldsValue 这个 props, 页面加载完就会设置好这些值,并且不会触发错误
10. 使用
1 | import autoBindForm from './autoBindForm' |
这里需要注意的是,如果使用 autoBindForm 包装过的组件 也就是
1 | <MyformPage defaultFieldsValue={defaultFieldsValue}/> |
这时候 想拿到 ref , 不要忘了 forwardRef
1 | this.ref = React.createRef() |
同理修改 ‘autoBindForm.js’
1 | render() { |
11. 最终代码
1 | import * as React from 'react'; |
12. 结语
这样一个 对 Form.create 再次包装的 高阶组件,解决了一定的痛点,少写了很多模板代码,虽然封装的时候遇到了各种各样奇奇怪怪的问题,但是都解决了,没毛病,也加强了我对高阶组件的认知,溜了溜了 :)
