骚操做: 基于 Antd Form 的高阶组件 AutoBindForm

1. 前言

好久没更新博客了, 皮的嘛,就不谈了,不过问题不大,今天就结合 项目中写的一个 React 高阶组件 的实例 再来说一讲,结合上一篇文章,加深一下印象html

2. Ant Design 的 Form 组件

国民组件库 Ant-DesignForm 库 想必你们都用过, 比较强大, 基于 rc-form 封装, 功能比较齐全react

最近项目中遇到了一个需求, 普通的一个表单, 表单字段没有 填完的时候, 提交按钮 是 disabled 状态的, 听起来很简单, 因为用的是 antd 翻了翻文档, copy 了一下代码 , 发现须要些很多的代码es6

Edit antd reproduction template

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>
    );
  }
}


复制代码

3. 那么问题来了

上面的代码咋一看没什么毛病, 给每一个字段绑定一个 validateStatus 去看当前字段 有没有触碰过 而且没有错, 并在 组件渲染的时候 触发一次验证, 经过这种方式 来达到 disabled 按钮的目的, 可是要命的 只是 实现一个 disabled 的效果, 多写了这么多的代码, 实际遇到的场景是 有10多个这种需求的表单,有没有什么办法不写这么多的模板代码呢? 因而我想到了 高阶组件api

4. 开始干活

因为 Form.create() 后 会给 this.props 添加 form 属性 ,从而使用它提供的 api, 通过观察 咱们预期想要的效果有如下几点bash

// 使用效果

@autoBindForm   //须要实现的组件
export default class FormPage extends React.PureComponent {
    
}

复制代码

要达到以下效果markdown

  • 1.componentDidMount 的时候 触发一次 字段验证
  • 2.这时候会出现错误信息, 这时候须要干掉错误信息
  • 3.而后遍历当前组件全部的字段, 判断 是否有错
  • 4.提供一个 this.props.hasError 相似的字段给当前组件.控制 按钮的 disabled 状态
  • 5.支持非必填字段, (igonre)
  • 6.支持编辑模式 (有默认值)

5. 实现 autoBindForm

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 的效果,

6. 实现实时的错误判断 hasError

因为子组件 须要一个 状态 来知道 当前的表单是否有错误, 因此咱们定义一个 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,
}
复制代码

写到这里, 建立表单的场景, 基本上能够用这个高阶组件轻松搞定, 可是有一些表单有一些非必填项, 这时就会出现,非必填项可是认为有错误的清空, 接下来, 改进一下代码

7. 优化组件, 支持 非必填字段

非必填字段, 即认为是一个配置项, 由调用者告诉我哪些是 非必填项, 当时我原本想搞成 自动去查找 当前组件哪些字段不是 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 验证一下当前表单, 这个函数 返回当前表单的错误值, 非必填的字段 此时不会有错误, 因此 只须要拿到当前错误信息, 和 全部字段 比较 二者 不一样的值, 使用 loadshxor 函数 完成

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)

      })
    }
复制代码

通过这样一波修改, 支持非必填字段的需求就算完成了

8. 最后一波, 支持默认字段

其实这个很简单, 就是看子组件是否有默认值 , 若是有 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, 页面加载完就会设置好这些值,而且不会触发错误

10. 使用

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} />),
)
复制代码

11. 最终代码

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

复制代码

12. 结语

这样一个 对 Form.create 再次包装的 高阶组件, 解决了必定的痛点, 少写了不少模板代码, 虽然封装的时候遇到了各类各样奇奇怪怪的问题,可是都解决了, 没毛病, 也增强了我对高阶组件的认知,溜了溜了 :)

相关文章
相关标签/搜索