1. Introduction

I haven’t updated my blog for a long time, so I won’t talk about it, but it’s not a big problem. Today I will talk about it in combination with an example of a high-level component of React written in the project. Combined with the last article, I will deepen my impression

2. The Ant Design Form component

National component library Ant-Design Form library we must have used, more powerful, based on RC-Form packaging, more complete functions

Recently, I encountered a requirement in the project. When the fields of a common form were not filled out, the submit button was disabled. It sounded simple, but antD was used

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: 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"; FormItem> <FormItem> <Button type="primary" HtmlType ="submit" disabled={hasErrors(getFieldsError())} > </Button> </FormItem> </Form>); }}Copy the code

3. So here’s the problem

The above code looks fine at first glance, but binds each field to a validateStatus to see if the current field has been touched and is correct, and triggers a validation when the component renders, thus enabling the disabled button. But the fatal is just to achieve a disabled effect, write so much code, the actual scenario is to have more than 10 such requirements of the form, there is no way not to write so much template code? So I came up with higher-order components

4. Get to work

Since form.create () adds a Form attribute to this.props to use the API it provides, we looked at the effects we expected

// Use effect

@autoBindForm   // Components to implement
export default class FormPage extends React.PureComponent {}Copy the code

To achieve the following effect

  • 1.componentDidMountTriggers a field validation when
  • 2. Error messages will appear and you need to kill them
  • 3. Then, iterate over all fields of the current component to check whether there is any error
  • 4. Provide onethis.props.hasErrorSimilar fields to the current component. Control buttondisabledstate
  • 5. Support non-mandatory fields, (igonre)
  • 6. Support edit mode (default value)

5. Implement 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)
  }
Copy the code

Create the components we need to wrap so that we don’t have to create each page once

Then we get a reference to the form through the wrappedComponentRef provided by ANTD

According to the ANTD documentation, we need the following API to achieve the desired effect

  • validateFieldsVerify the field
  • getFieldsValueGets the value of the field
  • setFieldsSets the value of the field
  • getFieldsErrorGets the error information for the field
  • isFieldTouchedGets whether the field has been touched
class AutoBindForm extends WrappedComponent
Copy the code

Inheriting the components we need to wrap (known as reverse inheritance), we can validate the fields at initialization time

componentDidMount(){
  const {
    form: {
        validateFields,
        getFieldsValue,
        setFields,
      },
   } = this.props

    validateFields()
  }
}
Copy the code

Since the user entered the page without input, you need to manually clear the error message

componentDidMount() {
    const {
      form: {
        validateFields,
        getFieldsValue,
        setFields,
      },
    } = this.props

    validateFields()

    Object.keys(getFieldsValue())
      .forEach((field) = > {
        setFields({
          [field]: {
            errors: null.status: null,},})})}}Copy the code

GetFieldsValue () allows you to dynamically retrieve all the fields in the current form, and then use setFields to iterate and set the error status of all fields to null, so that we achieve the effect of 1,2.

6. Realize real-time error judgment hasError

Since the child component needs a state to know if the current form has an error, we define a value of hasError to implement it. Since it’s real-time, it’s not hard to think of getters to implement it.

Those of you familiar with Vue may think of the computational properties implemented by object.DefinedPropty,

Essentially, the form field collection provided by Antd also triggers page rendering via setState. In the current scenario, the same effect can be achieved directly using the GET property supported by ES6


get hasError() {
    const {
      form: { getFieldsError, isFieldTouched }
    } = this.props
    
    let fieldsError = getFieldsError() as any
    
    return Object
      .keys(fieldsError)
      .some((field) = >! isFieldTouched(field) || fieldsError[field])) }Copy the code

The code is simple. Every time the getter is triggered, we use the some function to see if the current form has been touched or has an error. In the case of creating the form, if it has not been touched, it has not been entered, so there is no need to verify that there is an error

Finally, pass hasError to the child component at Render

  render() {
    return (
      <WrappedComponent
        wrappedComponentRef={this.getFormRef}
        {. this.props}
        hasError={this.hasError}
      />} // Parent console.log(this.prop.haserror)<Button disabled={this.props.hasError}>submit</Button>
Copy the code

And let’s define type

export interface IAutoBindFormHelpProps {
  hasError: boolean,
}
Copy the code

Now, the scenario where you create a form, basically, you can easily do it with this high-level component, but there are some forms that have some non-mandatory fields, and then you get a blank that doesn’t have to be filled but you think it’s wrong, so let’s improve the code

7. Optimize components to support non-mandatory fields

Optional fields are considered a configuration item and the caller tells me which fields are optional. At the time, I wanted to automatically find out which fields of the current component are not requried, but ANTD’s documentation looked like a mod, so I gave up

First modify the function and add a layer of currying

export default (filterFields: string[] = []) =>
  (WrappedComponent: React.ComponentClass<any>) = >{}Copy the code
@autoBindForm(['fieldA'.'fieldB']) // Components to implementexport default class FormPage extends React.PureComponent {
    
}
Copy the code

Modify hasError logic

    get hasError() {
      const {
        form: { getFieldsError, isFieldTouched, getFieldValue },
        defaultFieldsValue,
      } = this.props

      const { filterFields } = this.state
      constisEdit = !! defaultFieldsValuelet fieldsError = getFieldsError()

      const needOmitFields = filterFields.filter((field) = >! isFieldTouched(field)).concat(needIgnoreFields)if(! isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) }return Object
        .keys(fieldsError)
        .some((field) = > {
          constisCheckFieldTouched = ! isEdit || isEmpty(getFieldValue(field))returnisCheckFieldTouched ? (! isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) }Copy the code

The logic is pretty crude, you go through the field that you want to filter, see if it’s been touched, and if it has been touched, you don’t add error validation

Similarly, when you initialize it, you filter it,

First, get all fields of the current form through object.keys (getFieldsValue), because I don’t know which fields are requierd at this time, clever me

This function returns the error value of the current form. Non-required fields do not have errors, so you just need to get the current error information and compare the different values with all fields. Use the xOR function of loadsh to do this

    const filterFields = xor(fields, Object.keys(err || []))
    this.setState({
      filterFields,
    })
Copy the code

Finally, clear all error messages

Complete code:

 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)

      })
    }
Copy the code

With this wave of changes, the need to support non-mandatory fields is complete

8. The last wave supports default fields

This is as simple as seeing if the child component has a default value. If it has a setFieldsValue, it is done. The child component and the parent component agree on a defaultFieldsValue

The complete code is as follows

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[]
}

/ * * * @ * @ param name AutoBindForm needIgnoreFields string [] need to ignore validation fields * @ param {WrappedComponent. DefaultFieldsValue} Object form initial value */
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
      constisEdit = !! defaultFieldsValuelet fieldsError = getFieldsError()

      const needOmitFields = filterFields.filter((field) = >! isFieldTouched(field)).concat(needIgnoreFields)if(! isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) }return Object
        .keys(fieldsError)
        .some((field) = > {
          constisCheckFieldTouched = ! isEdit || isEmpty(getFieldValue(field))returnisCheckFieldTouched ? (! 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) / / as a result of inherited WrappedComponent so I can get WrappedComponent props the if (this. Props. DefaultFieldsValue) {  this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()(AutoBindForm) } export default autoBindFormCopy the code

This way, if the child component has the props for defaultFieldsValue, those values will be set when the page loads, and no error will be triggered

Use 10.

Import autoBindForm from './autoBindForm' class MyFormPage extends React.PureComponent {... @autobindForm (['filedsA','fieldsB']) class MyFormPage extends React.PureComponent {... Class MyFormPage extends React.pureComponent {// MyFormPage extends react.pureComponent {... // xx.js const defaultFieldsValue = {name: 'xx', age: 'xx', rangePicker: [moment(),moment()] } <MyformPage defaultFieldsValue={defaultFieldsValue} />Copy the code

The important thing to note here is that if the component is wrapped with autoBindForm, it is

<MyformPage defaultFieldsValue={defaultFieldsValue}/>
Copy the code

If you want to get the REF, don’t forget the forwardRef

this.ref = React.createRef()
<MyformPage defaultFieldsValue={defaultFieldsValue} ref={this.ref}/>

Copy the code

Do the same with ‘autobindform.js’

render() {
  const { forwardedRef, props } = this.props
  return( <WrappedComponent wrappedComponentRef={this.getFormRef} {... props} hasError={this.hasError} ref={forwardedRef} /> ) }returnForm.create()( React.forwardRef((props, ref) => <AutoBindForm {... props} forwardedRef={ref} />), )Copy the code

11. Final code

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[]
}

/ * * * @ * @ param name AutoBindForm needIgnoreFields string [] need to ignore validation fields * @ param {WrappedComponent. DefaultFieldsValue} Object form initial value */
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
      constisEdit = !! defaultFieldsValuelet fieldsError = getFieldsError()

      const needOmitFields = filterFields.filter((field) = >! isFieldTouched(field)).concat(needIgnoreFields)if(! isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) }return Object
        .keys(fieldsError)
        .some((field) = > {
          constisCheckFieldTouched = ! isEdit || isEmpty(getFieldValue(field))returnisCheckFieldTouched ? (! 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) / / attributes Initialize the default value if (this. Props. DefaultFieldsValue) { this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()( React.forwardRef((props, ref) => <AutoBindForm {... props} forwardedRef={ref} />), ) } export default autoBindFormCopy the code

12. Conclusion

Form. Create (Form. Create, Form. Create, Form. Create, Form