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
再次包装的 高阶组件,解决了一定的痛点,少写了很多模板代码,虽然封装的时候遇到了各种各样奇奇怪怪的问题,但是都解决了,没毛病,也加强了我对高阶组件的认知,溜了溜了 :)