好久没更新博客了, 皮的嘛,就不谈了,不过问题不大,今天就结合 项目中写的一个 React 高阶组件 的实例 再来说一讲,结合上一篇文章,加深一下印象html
国民组件库 Ant-Design
的 Form
库 想必你们都用过, 比较强大, 基于 rc-form
封装, 功能比较齐全react
最近项目中遇到了一个需求, 普通的一个表单, 表单字段没有 填完的时候, 提交按钮 是 disabled
状态的, 听起来很简单, 因为用的是 antd
翻了翻文档, copy 了一下代码 , 发现须要些很多的代码es6
import { Form, Icon, Input, Button } from 'antd'; const FormItem = Form.Item; function hasErrors(fieldsError) { return Object.keys(fieldsError).some(field => fieldsError[field]); } @Form.create(); class Page extends React.Component<{},{}> { componentDidMount() { this.props.form.validateFields(); } handleSubmit = (e: React.FormEvent<HTMLButtonElement>) => { e.preventDefault(); this.props.form.validateFields((err:any, values:any) => { if (!err) { ... } }); } render() { const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form; const userNameError = isFieldTouched('userName') && getFieldError('userName'); const passwordError = isFieldTouched('password') && getFieldError('password'); return ( <Form layout="inline" onSubmit={this.handleSubmit}> <FormItem validateStatus={userNameError ? 'error' : ''} help={userNameError || ''} > {getFieldDecorator('userName', { rules: [{ required: true, message: 'Please input your username!' }], })( <Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" /> )} </FormItem> <FormItem validateStatus={passwordError ? 'error' : ''} help={passwordError || ''} > {getFieldDecorator('password', { rules: [{ required: true, message: 'Please input your Password!' }], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" /> )} </FormItem> <FormItem> <Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())} > 登陆 </Button> </FormItem> </Form> ); } } 复制代码
上面的代码咋一看没什么毛病, 给每一个字段绑定一个 validateStatus
去看当前字段 有没有触碰过 而且没有错, 并在 组件渲染的时候 触发一次验证, 经过这种方式 来达到 disabled
按钮的目的, 可是要命的 只是 实现一个 disabled
的效果, 多写了这么多的代码, 实际遇到的场景是 有10多个这种需求的表单,有没有什么办法不写这么多的模板代码呢? 因而我想到了 高阶组件
api
因为 Form.create()
后 会给 this.props
添加 form
属性 ,从而使用它提供的 api, 通过观察 咱们预期想要的效果有如下几点bash
// 使用效果 @autoBindForm //须要实现的组件 export default class FormPage extends React.PureComponent { } 复制代码
要达到以下效果markdown
componentDidMount
的时候 触发一次 字段验证this.props.hasError
相似的字段给当前组件.控制 按钮的 disabled
状态import * as React from 'react' import { Form } from 'antd' const getDisplayName = (component: React.ComponentClass) => { return component.displayName || component.name || 'Component' } export default (WrappedComponent: React.ComponentClass<any>) => { class AutoBindForm extends WrappedComponent { static displayName = `HOC(${getDisplayName(WrappedComponent)})` autoBindFormHelp: React.Component<{}, {}> = null getFormRef = (formRef: React.Component) => { this.autoBindFormHelp = formRef } render() { return ( <WrappedComponent wrappedComponentRef={this.getFormRef} /> ) } return Form.create()(AutoBindForm) } 复制代码
首先 Form.create
一下咱们须要包裹的组件, 这样就不用每个页面都要 create
一次antd
而后咱们经过 antd
提供的 wrappedComponentRef
拿到了 form
的引用app
根据 antd
的文档 ,咱们要实现想要的效果,须要用到 以下 api函数
validateFields
验证字段getFieldsValue
获取字段的值setFields
设置字段的值getFieldsError
获取字段的错误信息isFieldTouched
获取字段是否触碰过class AutoBindForm extends WrappedComponent
复制代码
继承咱们须要包裹的组件(也就是所谓的反向继承), 咱们能够 在初始化的时候 验证字段oop
componentDidMount(){ const { form: { validateFields, getFieldsValue, setFields, }, } = this.props validateFields() } } 复制代码
因为进入页面时 用户并无输入, 因此须要手动清空 错误信息
componentDidMount() { const { form: { validateFields, getFieldsValue, setFields, }, } = this.props validateFields() Object.keys(getFieldsValue()) .forEach((field) => { setFields({ [field]: { errors: null, status: null, }, }) }) } } 复制代码
经过 getFieldsValue()
咱们能够动态的拿到当前 表单 全部的字段, 而后再使用 setFields
遍历一下 把全部字段的 错误状态设为 null
, 这样咱们就实现了 1,2 的效果,
因为子组件 须要一个 状态 来知道 当前的表单是否有错误, 因此咱们定义一个 hasError
的值 来实现, 因为要是实时的,因此不难想到用 getter
来实现,
熟悉Vue
的同窗 可能会想到 Object.definedPropty
实现的 计算属性,
本质上 Antd
提供的 表单字段收集也是经过 setState
, 回触发页面渲染, 在当前场景下, 直接使用 es6
支持的get
属性便可实现一样的效果 代码以下
get hasError() { const { form: { getFieldsError, isFieldTouched } } = this.props let fieldsError = getFieldsError() as any return Object .keys(fieldsError) .some((field) => !isFieldTouched(field) || fieldsError[field])) } 复制代码
代码很简单 ,在每次 getter
触发的时候, 咱们用 some
函数 去判断一下 当前的表单是否触碰过 或者有错误, 在建立表单这个场景下, 若是没有触碰过,必定是没输入,因此没必要验证是否有错
最后 在 render
的时候 将 hasError
传给 子组件
render() { return ( <WrappedComponent wrappedComponentRef={this.getFormRef} {...this.props} hasError={this.hasError} /> ) } //父组件 console.log(this.prop.hasError) <Button disabled={this.props.hasError}>提交</Button> 复制代码
同时咱们定义下 type
export interface IAutoBindFormHelpProps { hasError: boolean, } 复制代码
写到这里, 建立表单的场景, 基本上能够用这个高阶组件轻松搞定, 可是有一些表单有一些非必填项, 这时就会出现,非必填项可是认为有错误的清空, 接下来, 改进一下代码
非必填字段, 即认为是一个配置项, 由调用者告诉我哪些是 非必填项, 当时我原本想搞成 自动去查找 当前组件哪些字段不是 requried
的, 可是 antd
的文档貌似 莫得, 就放弃了
首先修改函数, 增长一层柯里化
export default (filterFields: string[] = []) => (WrappedComponent: React.ComponentClass<any>) => { } 复制代码
@autoBindForm(['fieldA','fieldB']) //须要实现的组件 export default class FormPage extends React.PureComponent { } 复制代码
修改 hasError
的逻辑
get hasError() { const { form: { getFieldsError, isFieldTouched, getFieldValue }, defaultFieldsValue, } = this.props const { filterFields } = this.state const isEdit = !!defaultFieldsValue let fieldsError = getFieldsError() const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields) if(!isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) } return Object .keys(fieldsError) .some((field) => { const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field)) return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) } 复制代码
逻辑很简单粗暴, 遍历一下须要过滤的字段,看它有没有触碰过,若是触碰过,就不加入错误验证
同理, 在 初始化的时候也过滤一下,
首先经过 Object.keys(getFieldsValue)
拿到当前表单 的全部字段, 因为 这时候不知道哪些字段 是 requierd
的, 机智的我
validateFields
验证一下当前表单, 这个函数 返回当前表单的错误值, 非必填的字段 此时不会有错误, 因此 只须要拿到当前错误信息, 和 全部字段 比较 二者 不一样的值, 使用 loadsh
的 xor
函数 完成
const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) 复制代码
最后清空 全部错误信息
完整代码:
componentDidMount() { const { form: { validateFields, getFieldsValue, getFieldValue, setFields, }, } = this.props const fields = Object.keys(getFieldsValue()) validateFields((err: object) => { const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) const allFields: { [key: string]: any } = {} fields .filter((field) => !filterFields.includes(field)) .forEach((field) => { allFields[field] = { value: getFieldValue(field), errors: null, status: null, } }) setFields(allFields) }) } 复制代码
通过这样一波修改, 支持非必填字段的需求就算完成了
其实这个很简单, 就是看子组件是否有默认值 , 若是有 setFieldsValue
一下就搞定了, 子组件和父组件约定一个 defaultFieldsValue
完整代码以下
import * as React from 'react' import { Form } from 'antd' import { xor, isEmpty, omit } from 'lodash' const getDisplayName = (component: React.ComponentClass) => { return component.displayName || component.name || 'Component' } export interface IAutoBindFormHelpProps { hasError: boolean, } interface IAutoBindFormHelpState { filterFields: string[] } /** * @name AutoBindForm * @param needIgnoreFields string[] 须要忽略验证的字段 * @param {WrappedComponent.defaultFieldsValue} object 表单初始值 */ const autoBindForm = (needIgnoreFields: string[] = [] ) => (WrappedComponent: React.ComponentClass<any>) => { class AutoBindForm extends WrappedComponent { get hasError() { const { form: { getFieldsError, isFieldTouched, getFieldValue }, defaultFieldsValue, } = this.props const { filterFields } = this.state const isEdit = !!defaultFieldsValue let fieldsError = getFieldsError() const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields) if(!isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) } return Object .keys(fieldsError) .some((field) => { const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field)) return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) } static displayName = `HOC(${getDisplayName(WrappedComponent)})` state: IAutoBindFormHelpState = { filterFields: [], } autoBindFormHelp: React.Component<{}, {}> = null getFormRef = (formRef: React.Component) => { this.autoBindFormHelp = formRef } render() { return ( <WrappedComponent wrappedComponentRef={this.getFormRef} {...this.props} hasError={this.hasError} /> ) } componentDidMount() { const { form: { validateFields, getFieldsValue, getFieldValue, setFields, }, } = this.props const fields = Object.keys(getFieldsValue()) validateFields((err: object) => { const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) const allFields: { [key: string]: any } = {} fields .filter((field) => !filterFields.includes(field)) .forEach((field) => { allFields[field] = { value: getFieldValue(field), errors: null, status: null, } }) setFields(allFields) // 因为继承了 WrappedComponent 因此能够拿到 WrappedComponent 的 props if (this.props.defaultFieldsValue) { this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()(AutoBindForm) } export default autoBindForm 复制代码
这样一来, 若是子组件 有 defaultFieldsValue
这个 props, 页面加载完就会设置好这些值,而且不会触发错误
import autoBindForm from './autoBindForm'
# 基本使用
@autoBindForm()
class MyFormPage extends React.PureComponent {
...没有灵魂的表单代码
}
# 忽略字段
@autoBindForm(['filedsA','fieldsB'])
class MyFormPage extends React.PureComponent {
...没有灵魂的表单代码
}
# 默认值
// MyFormPage.js
@autoBindForm()
class MyFormPage extends React.PureComponent {
...没有灵魂的表单代码
}
// xx.js
const defaultFieldsValue = {
name: 'xx',
age: 'xx',
rangePicker: [moment(),moment()]
}
<MyformPage defaultFieldsValue={defaultFieldsValue} />
复制代码
这里须要注意的是, 若是使用 autoBindForm
包装过的组件 也就是
<MyformPage defaultFieldsValue={defaultFieldsValue}/>
复制代码
这时候 想拿到 ref
, 不要忘了 forwardRef
this.ref = React.createRef()
<MyformPage defaultFieldsValue={defaultFieldsValue} ref={this.ref}/>
复制代码
同理修改 'autoBindForm.js'
render() { const { forwardedRef, props } = this.props return ( <WrappedComponent wrappedComponentRef={this.getFormRef} {...props} hasError={this.hasError} ref={forwardedRef} /> ) } return Form.create()( React.forwardRef((props, ref) => <AutoBindForm {...props} forwardedRef={ref} />), ) 复制代码
import * as React from 'react' import { Form } from 'antd' import { xor, isEmpty, omit } from 'lodash' const getDisplayName = (component: React.ComponentClass) => { return component.displayName || component.name || 'Component' } export interface IAutoBindFormHelpProps { hasError: boolean, } interface IAutoBindFormHelpState { filterFields: string[] } /** * @name AutoBindForm * @param needIgnoreFields string[] 须要忽略验证的字段 * @param {WrappedComponent.defaultFieldsValue} object 表单初始值 */ const autoBindForm = (needIgnoreFields: string[] = []) => (WrappedComponent: React.ComponentClass<any>) => { class AutoBindForm extends WrappedComponent { get hasError() { const { form: { getFieldsError, isFieldTouched, getFieldValue }, defaultFieldsValue, } = this.props const { filterFields } = this.state const isEdit = !!defaultFieldsValue let fieldsError = getFieldsError() const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields) if (!isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) } return Object .keys(fieldsError) .some((field) => { const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field)) return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) } static displayName = `HOC(${getDisplayName(WrappedComponent)})` state: IAutoBindFormHelpState = { filterFields: [], } autoBindFormHelp: React.Component<{}, {}> = null getFormRef = (formRef: React.Component) => { this.autoBindFormHelp = formRef } render() { const { forwardedRef, props } = this.props return ( <WrappedComponent wrappedComponentRef={this.getFormRef} {...props} hasError={this.hasError} ref={forwardedRef} /> ) } componentDidMount() { const { form: { validateFields, getFieldsValue, getFieldValue, setFields, }, } = this.props const fields = Object.keys(getFieldsValue()) validateFields((err: object) => { const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) const allFields: { [key: string]: any } = {} fields .filter((field) => !filterFields.includes(field)) .forEach((field) => { allFields[field] = { value: getFieldValue(field), errors: null, status: null, } }) setFields(allFields) // 属性劫持 初始化默认值 if (this.props.defaultFieldsValue) { this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()( React.forwardRef((props, ref) => <AutoBindForm {...props} forwardedRef={ref} />), ) } export default autoBindForm 复制代码
这样一个 对 Form.create
再次包装的 高阶组件, 解决了必定的痛点, 少写了不少模板代码, 虽然封装的时候遇到了各类各样奇奇怪怪的问题,可是都解决了, 没毛病, 也增强了我对高阶组件的认知,溜了溜了 :)