騷操做: 基於 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 {
    
}

複製代碼

要達到以下效果antd

  • 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 一次app

而後咱們經過 antd 提供的 wrappedComponentRef 拿到了 form 的引用函數

根據 antd 的文檔 ,咱們要實現想要的效果,須要用到 以下 api優化

  • validateFields 驗證字段
  • getFieldsValue 獲取字段的值
  • setFields 設置字段的值
  • getFieldsError 獲取字段的錯誤信息
  • isFieldTouched 獲取字段是否觸碰過
class AutoBindForm extends WrappedComponent
複製代碼

繼承咱們須要包裹的組件(也就是所謂的反向繼承), 咱們能夠 在初始化的時候 驗證字段ui

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 再次包裝的 高階組件, 解決了必定的痛點, 少寫了不少模板代碼, 雖然封裝的時候遇到了各類各樣奇奇怪怪的問題,可是都解決了, 沒毛病, 也增強了我對高階組件的認知,溜了溜了 :)

相關文章
相關標籤/搜索