Операция Sao: компонент высшего порядка на основе Antd Form AutoBindForm

Ant Design

1. Введение

Я давно не обновлял свой блог, поэтому я не буду говорить об этом, но это не большая проблема.Сегодня я расскажу об этом на примере высокоуровневого компонента React, написанного в проекте, и объедините ее с предыдущей статьей, чтобы усилить мое впечатление.

2. Компонент форм Ant Design

Национальная библиотека компонентовAnt-DesignизFormБиблиотека наверняка использовалась всеми, она относительно мощная, основана наrc-formПакет, полная функция

Недавно в проекте столкнулся с требованием, обычная форма, когда поля формы не заполнены, кнопка отправкиdisabledСтатус, звучит просто, потому что использованиеantdЯ пролистал документацию, скопировал код и обнаружил, что мне нужно много кода

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 форм с этим требованием. Есть ли способ избежать написания такого большого количества шаблонного кода? Вот я и подумал об этом.高阶组件

4. Приступайте к работе

из-заForm.create()отдам позжеthis.propsДобавить кformсвойства, чтобы использовать API, который он предоставляет, после наблюдения мы ожидаем следующих эффектов

// 使用效果

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

Для достижения следующего эффекта

  • 1.componentDidMountПри запуске проверки поля
  • 2. В это время появится сообщение об ошибке, и вам нужно избавиться от сообщения об ошибке в это время.
  • 3. Затем пройдитесь по всем полям текущего компонента, чтобы определить, есть ли ошибка
  • 4. Обеспечьтеthis.props.hasErrorТекущему компоненту присваиваются аналогичные поля.disabledусловие
  • 5. Поддерживать необязательные поля (игнорировать)
  • 6. Поддержка режима редактирования (со значением по умолчанию)

5. Реализуйте автопривязку формы

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который предоставилwrappedComponentRefпонятноformцитаты

согласно сantd, нам нужно использовать следующий API для достижения желаемого эффекта

  • validateFieldsполе проверки
  • getFieldsValueПолучить значение поля
  • setFieldsустановить значение поля
  • getFieldsErrorПолучить информацию об ошибке поля
  • isFieldTouchedПолучить, было ли затронуто поле
class AutoBindForm extends WrappedComponent

Наследуя компоненты, которые нам нужно обернуть (так называемое обратное наследование), мы можем проверять поля во время инициализации

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>

При этом определяем тип

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Подтвердите текущую форму, эта функция возвращает значение ошибки текущей формы, необязательные поля не будут иметь ошибок в это время, поэтому вам нужно только получить текущую информацию об ошибке, сравнить разные значения двух со всеми поля, использовать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)

      })
    }

После такой волны модификаций требование поддержки необязательных полей выполнено.

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Этот реквизит, эти значения будут установлены при загрузке страницы и не вызовут ошибку

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Переупакованные высокоуровневые компоненты решили некоторые болевые точки и сэкономили много шаблонного кода.Хотя я столкнулся со всеми видами странных проблем во время упаковки, все они были решены, никаких проблем, и это также укрепило мое понимание высокоуровневых компонентов. компонентов заказа, ускользнуло :)