preface

Recently, with a very complex form requirement that you might need to do “anything” with a form, the existing UI component library of choice is Ant Design or ANTD. Its Form Form encapsulates common operations such as “Form validation” and “Form error message”. It’s very easy to use. I looked at the antD Form source code and found that its core capabilities are provided through the RC-Field-Form library. So reading its source code will be a must before the author starts his project. This article will simulate the RC-field-form library and write a “learning version” to study its ideas in depth.

If this article can help you, please go to 👍!

Engineering structures,

Rc-field-form uses Dumi and father-build to package the component library. For consistency, the author will also use these two tools to complete the project.

Dumi

Dumi is a documentation tool for component development scenarios. It provides a one-stop component development experience with father-Builder, which is responsible for building components, and Dumi, which is responsible for component development and document generation.

father-build

Father-build is part of Father, a document and component packaging library that focuses on component packaging.

Scaffold creation project

Use @umijs/create-dumi-lib to initialize the project. This scaffold is a combination of the two tools mentioned above.

mkdir lion-form Create the lion-form folder
cd lion-form // Go to the folder
npm init -y // Initialize package.json
npx @umijs/create-dumi-lib // Initialize the overall project structure
Copy the code

Description of project Structure

├ ─ ─ the README, md// Documentation├ ─ ─ node_modules// Dependency package folder├ ─ ─ package. Json// NPM package management├ ─ ─ editorconfig// Editor style unified configuration file├ ─ ─ fatherrc. Ts// Package the configuration├ ─ ─ umirc. Ts// Document configuration├ ─ ─ prettierrc// Text formatting configuration├ ─ ─ tsconfig. Json/ / ts configuration└ ─ ─ the docs// Store public documents└ ─ ─ index. The md// The home page of the component library document└ ─ ─ the SRC └ ─ ─ index. Js// Component library entry file
Copy the code

Start the project

NPM start or YARN startCopy the code



The documentation, packaged as one component library is built quickly. So let’s write one by handrc-field-form.



Full code address

The source code to write

rc-field-form

Antd is familiar to students who use React development frequently. Most forms commonly encountered in development are completed by antD Form series components, and RC-field-Form is an important part of ANTD Form, or ANTD Form is the further encapsulation of RC-field-Form. If you want to learn its source code, you must first know how to use it, otherwise it is difficult to understand some of the deep meaning of the source code.

Simple example

Start by implementing the form shown below, similar to the login registration page we wrote about.





Code examples:

import React, { Component, useEffect} from 'react'
import Form, { Field } from 'rc-field-form'
import Input from './Input'
// Name field verification rule
const nameRules = {required: true.message: 'Please enter your name! '}
// Password field verification rule
const passwordRules = {required: true.message: 'Please enter your password! '}

export default function FieldForm(props) {
  // Get the form instance
  const [form] = Form.useForm()
  // Triggered when the form is submitted
  const onFinish = (val) = > {
    console.log('onFinish', val)
  }
  // Triggered when submitting the form fails
  const onFinishFailed = (val) = > {
    console.log('onFinishFailed', val)
  }
  // When the component is initialized, it is a React native Hook
  useEffect(() = > {
    form.setFieldsValue({username: 'lion'}}), [])return (
    <div>
      <h3>FieldForm</h3>
      <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
        <Field name='username' rules={[nameRules]}>
          <Input placeholder='Please enter your name' />
        </Field>
        <Field name='password' rules={[passwordRules]}>
          <Input placeholder='Please enter your password' />
        </Field>
        <button>Submit</button>
      </Form>
    </div>)}// Input simple encapsulation
const Input = (props) = > {
  const{ value,... restProps } = props;return <input {. restProps} value={value} />;
};
Copy the code

This is a very convenient way to write it, and there is no need to wrap a layer with higher-order functions like ANTd3. Instead, we get the formInstance instance directly from form.useform (), which holds all the data and methods needed by the Form. Form.setfieldsvalue ({username: ‘lion’}) you can manually set the initial value of username by using the form. It can also be understood that all form items are taken over by a formInstance instance. You can use the formInstance instance to do anything you want with a form item. The formInstance instance is also the core of the library.

Foundation frame construction

Through the rC-field-form source code learning, we first set up a basic framework.

useForm

  • throughForm.useForm()To obtainformInstanceInstance;
  • formInstanceThe instance provides global methods such assetFieldsValue 、 getFieldsValue ;
  • throughcontextMake it globally shareableformInstanceInstance.


src/useForm.tsx 

import React , {useRef} from "react";

class FormStore {
  // stroe is used to store form data in the format {"username": "lion"}
  private store: any = {};
  // Is used to store instance data for each Field, so each form item can be queried in the store via fieldEntities
  private fieldEntities: any = [];

  // Register the form item to fieldEntities
  registerField = (entity:any) = >{
    this.fieldEntities.push(entity)
    return () = > {
      this.fieldEntities = this.fieldEntities.filter((item:any) = >item ! == entity)delete this.store[entity.props.name]
    }
  }
  // Get a single field value
  getFieldValue = (name:string) = > {
    return this.store[name]
  }
  // Get all field values
  getFieldsValue = () = > {
    return this.store
  }
  // Set the value of the field
  setFieldsValue = (newStore:any) = > {
    // Update the store value
    this.store = { ... this.store, ... newStore, }// Retrieve all form items from fieldEntities, and iterate through the onStoreChange method to update the form item
    this.fieldEntities.forEach((entity:any) = > {
      const { name } = entity.props
      Object.keys(newStore).forEach(key= > {
        if (key === name) {
          entity.onStoreChange()
        }
      })
    })
  }
  // Submit data, which simply prints the data in the store.
  submit = () = >{
    console.log(this.getFieldsValue());
  }
  // Provide the FormStore instance method
  getForm = (): any= > ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    setFieldsValue: this.setFieldsValue,
    registerField: this.registerField,
    submit: this.submit,
  });
}
// Create a singleton formStore
export default function useForm(form:any) {
  const formRef = useRef();
  if(! formRef.current) {if (form) {
      formRef.current = form;
    } else {
      const formStore = new FormStore();
      formRef.current = formStore.getForm() asany; }}return [formRef.current]
}
Copy the code

FormStore is used to store global data and methods. UseForm exposes FormStore instances. As you can see from the useForm implementation, useRef implements the singleton pattern of FormStore instances.

FieldContext

Defines the global context.

import * as React from 'react';

const warningFunc: any = () = > {
  console.log("warning");
};

const Context = React.createContext<any>({
  getFieldValue: warningFunc,
  getFieldsValue: warningFunc,
  setFieldsValue: warningFunc,
  registerField: warningFunc,
  submit: warningFunc,
});

export default Context;
Copy the code

The Form component

  • passFieldContext;
  • Intercept processingsubmitEvents;
  • Render child nodes.


src/Form.tsx 

import React from "react";
import useForm from "./useForm";
import FieldContext  from './FieldContext';

export default function Form(props:any) {
  const{form, children, ... restProps} = props;const [formInstance] = useForm(form) as any;
	
  return <form
    {. restProps}
    onSubmit={(event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); event.stopPropagation(); // Call the formInstance method formInstance.submit(); }} > {/* pass formInstance as a global context */}<FieldContext.Provider value={formInstance}>{children}</FieldContext.Provider>
  </form>
}
Copy the code

The Field component

  • Register yourself toFormStore;
  • Intercepts the child element for its additionvalueAs well asonChangeProperties.


src/Field.tsx 

import React,{Component} from "react";
import FieldContext from "./FieldContext";

export default class Field extends Component {
  // Field component gets FieldContext
  static contextType = FieldContext;

  private cancelRegisterFunc:any;
  // When Field is mounted, register yourself in FieldContext (fieldEntities array).
  componentDidMount() {
    const { registerField } = this.context;
    this.cancelRegisterFunc = registerField(this);
  }
  // When the Field component is unregistered, it is called to remove it from fieldEntities.
  componentWillUnmount() {
    if (this.cancelRegisterFunc) {
      this.cancelRegisterFunc()
    }
  }
  // Each Field component should contain the onStoreChange method to update itself
  onStoreChange = () = > {
    this.forceUpdate()
  }
  // The child element passed in the Field becomes a controlled component, which actively adds the value and onChange property methods
  getControlled = () = > {
    const { name } = this.props as any;
    const { getFieldValue, setFieldsValue } = this.context
    return {
      value: getFieldValue(name),
      onChange: (event:any) = > {
        const newValue = event.target.value
        setFieldsValue({[name]: newValue})
      },
    }
  }
	
  render() {
    const {children} = this.props as any;
    return React.cloneElement(children, this.getControlled())
  }
}
Copy the code

This completes the basic framework for the Form component, which can now implement some simple effects. Let’s write an example in the Docs directory. docs/examples/basic.tsx

. Part of the code is omittedexport default function BasicForm(props) {
  const [form] = Form.useForm()

  useEffect(() = > {
    form.setFieldsValue({username: 'lion'}}), [])return (
    <Form form={form}>
      <Field name='username'>
        <Input placeholder='Please enter your name' />
      </Field>
      <Field name='password'>
        <Input placeholder='Please enter your password' />
      </Field>
      <button>submit</button>
    </Form>)}Copy the code

Resolution:

  1. Called when the component is initializedform.setFieldsValue({username: 'lion'})Methods;
  2. setFieldsValueUpdated based on the parameters passed instoreValue and passnameFind the correspondingFieldInstance;
  3. callFieldThe instanceonStoreChangeMethod to update the component;
  4. Component updates, and the initial values are displayed on the screen.






Click to see the code in this section

Form

The Form component retrieves the ref

The ANTD documentation says: “We recommend using form.useform to create a Form data field for control. If it’s in class Component, you can also get the data field by ref. The usage is as follows:

export default class extends React.Component {
  formRef = React.createRef()

  componentDidMount() {
    this.formRef.current.setFieldsValue({username: 'lion'})}render() {
    return (
      <Form ref={this.formRef}>
      	<Field name='username'>
        	<Input />
        </Field>
        <Field name='password'>
          <Input />
        </Field>
          <button>Submit</button>
       </Form>)}}Copy the code

Pass the formRef to the Form component. Get the ref instance of the Form, but we know that the Form is created by the function component, which has no instance and cannot receive the ref as the class component can. Use the React. ForwardRef and useImperativeHandle.

src/Form.tsx 

export default React.forwardRef((props: any, ref) = >{... omitconst [formInstance] = useForm(form) as any;

  React.useImperativeHandle(ref, () = >formInstance); . Omit})Copy the code
  • React.forwardRefFunction components have no instances and cannot receive them as class components canrefAttribute problems;
  • useImperativeHandleYou can use it againrefWhen deciding what to expose to the parent component, here we willformInstanceExposed so that the parent component can be usedformInstance.

React Hooks: From Getting started with Applications to writing custom Hooks Click to see the code in this section

The initial value initialValues

We used to initialize form values like this:

useEffect(() = > {
  form.setFieldsValue({username: 'lion'}}), [])Copy the code

The initialValues attribute is provided so that we can initialize the form item. Let’s support it. src/useForm.ts

class FormStore {
  // Define the initial value variable
  private initialValues = {}; 

  setInitialValues = (initialValues:any,init:boolean) = >{
    // Assign the initial value to the initialValues variable so that formInstance always holds an initial value
    this.initialValues = initialValues;
    // synchronize to store
    if(init){
      // setValues is a utility class provided by rc-field-form
      SetValues recursively iterates through initialValues to return a new object.
      this.store = setValues({}, initialValues, this.store);
    }
  }	
  
  getForm = (): any= >({... The external use method is omitted// Create a method that returns some of the methods used internally
    getInternalHooks:() = >{
      return {
        setInitialValues: this.setInitialValues,
      }
    }
  });
}
Copy the code

src/Form.tsx 

export default React.forwardRef((props: any, ref) = > {
  const [formInstance] = useForm(form) as any;
  const {
    setInitialValues,
  } = formInstance.getInternalHooks();
  
  // setInitialValues for the first rendering the second parameter is true, indicating initialization. The second argument will be false for each subsequent rendering
  const mountRef = useRef(null) asany; setInitialValues(initialValues, ! mountRef.current);if(! mountRef.current) { mountRef.current =true; }... }Copy the code

UseRef returns a mutable REF object whose current property is initialized as the passed parameter (initialValue). The ref object returned remains constant throughout the life of the component.

Click to see the code in this section

submit

Before submitting submit, it could only print values from the store, which didn’t meet our requirement that it could call back to the specified function. src/useForm.ts

class FormStore {
  private callbacks = {} as any; // Hold the callback method

  / / set callbases
  setCallbacks = (callbacks:any) = > {
    this.callbacks = callbacks;
  }

  // Expose the setCallbacks method globally
  getForm = (): any= >({...getInternalHooks: () = > {
      return {
        setInitialValues: this.setInitialValues,
        setCallbacks: this.setCallbacks }; }});// Submit, go to the callbacks to retrieve the required callback method execution
  submit = () = > {
    const { onFinish } = this.callbacks;
    onFinish(this.getFieldsValue())
  };
}
Copy the code

src/Form.tsx

export default React.forwardRef((props: any, ref) = > {
  const{... , onFinish, ... restProps } = props;const [formInstance] = useForm(form) as any;
  const {
    setCallbacks,
  } = formInstance.getInternalHooks();
  // Get the external onFinish function, register it with callbacks, so it will be executed when submit
  setCallbacks({
    onFinish
  })
  
  ...
}
Copy the code

Click to see the code in this section

Field

shouldUpdate

The update logic of the Field is controlled via the shouldUpdate attribute. When shouldUpdate is a method, this method is called on every numeric update of the form, providing the old value with the current value so you can compare whether an update is needed.

src/Field.tsx 

export default class Field extends Component {
  ShouldUpdate = shouldUpdate = shouldUpdate = shouldUpdate ();
  onStoreChange = (prevStore:any,curStore:any) = > {
    const { shouldUpdate } = this.props as any;
    if (typeof shouldUpdate === 'function') {
      if(shouldUpdate(prevStore,curStore)){
        this.forceUpdate(); }}else{
      this.forceUpdate(); }}}Copy the code

src/useForm.js 

class FormStore {
  // We wrote a registerField to set the storage of the Field instance, and added a method to get it
  getFieldEntities = () = >{
    return this.fieldEntities;
  }
  // Add a method to notify the Field component of updates
  notifyObservers = (prevStore:any) = > {
    this.getFieldEntities().forEach((entity: any) = > {
      const { onStoreChange } = entity;
      onStoreChange(prevStore,this.getFieldsValue());
    });
  }
  // Now call notifyObservers directly to update the component after setting the field values
  setFieldsValue = (curStore: any) = > {
    const prevStore = this.store;
    if (curStore) {
      this.store = setValues(this.store, curStore);
    }
    this.notifyObservers(prevStore);
  };  
}
Copy the code

Well, the updated logic is almost done, not exactly the same as the original library (which takes into account more boundary conditions), but enough to help us understand the idea. Click to see the code in this section

Form validation

Validates the form and reports errors when it is submitted or at any other time, based on validation rules set by the user. Async-validator: Async-Validator: async-Validator: Async-Validator: Async-Validator: Async-Validator

async-validator

It is a library that performs asynchronous validation of data. Both ant.design and the Form component of Element UI use it for low-level validation.

The installation

npm i async-validator
Copy the code

Basic usage

import AsyncValidator from 'async-validator'
// Check rules
const descriptor = {
  username: [{required: true.message: 'Please fill in user name'
    },
    {
      pattern: /^\w{6}$/
      message: 'Username length is 6'}}]// Construct a validator based on the validation rules
const validator = new AsyncValidator(descriptor)
const data = {
  username: 'username'
}
validator.validate(data).then(() = > {
  // The check is successful
}).catch(({ errors, fields }) = > {
  // Verification failed
});
Copy the code

See the Github documentation for details on how async-Validator is used.

Field Component sets verification rules

    <Field
	label="Username"
	name="username"
	rules={[
           { required: true.message: 'Please input your username! ' },
           { pattern: /^\w{6}$/}}] ><Input />
    </Form.Item>
Copy the code

If the verification fails, the onFinishFailed callback function is executed. [Note] The library also supports setting custom validation functions in Rules, which are omitted from this component.

Component transformation

src/useForm.ts 

class FormStore {
  // Field validation
  validateFields = () = >{
    // The promise used to store field validation results
    const promiseList:any = [];
    // Iterate over the Field instance, invoke the validation method of the Field component, get the returned promise, and push it into the promiseList
    this.getFieldEntities().forEach((field:any) = >{
      const {name, rules} = field.props
      if(! rules || ! rules.length) {return;
      }
      const promise = field.validateRules();
      promiseList.push(
        promise
          .then(() = > ({ name: name, errors: [] }))
          .catch((errors:any) = >
            Promise.reject({
              name: name,
              errors,
            }),
          ),
      );
    })
    // allPromiseFinish is a utility method that handles the promiseList list as a promise
    PromiseList = reject (reject); promiseList = reject (reject); promiseList = reject (reject); promiseList = reject (reject
    const summaryPromise = allPromiseFinish(promiseList);
    const returnPromise = summaryPromise
      .then(
        () = > {
          return Promise.resolve(this.getFieldsValue());
        },
      )
      .catch((results) = > {
        // The merged promise returns an error if it is reject
        const errorList = results.filter((result:any) = > result && result.errors.length);
        return Promise.reject({
          values: this.getFieldsValue(),
          errorFields: errorList
        });
      });

    // Catch an error
    returnPromise.catch(e= > e);
	
    return returnPromise;
  }
  // The field validation method is invoked when the form is submitted, and the validation passes the onFinish callback, and the validation fails the onFinishFailed callback
  submit = () = > {
    this.validateFields()
      .then(values= > {
        const { onFinish } = this.callbacks;
        if (onFinish) {
          try {
            onFinish(values);
          } catch (err) {
            console.error(err);
          }
        }
      })
      .catch(e= > {
        const { onFinishFailed } = this.callbacks;
        if(onFinishFailed) { onFinishFailed(e); }}); }; }Copy the code

The core issue now is how the Field component retrieves validation results based on values and rules. src/Field.tsx

export default class Field extends Component {
  private validatePromise: Promise<string[]> | null = null
  private errors: string[] = [];
  // The Field component checks functions against rules
  validateRules = () = >{
    const { getFieldValue } = this.context;
    const { name } = this.props as any;
    const currentValue = getFieldValue(name); // Get the current value
    // The async-Validator library validates as a promise
    const rootPromise = Promise.resolve().then(() = > {
      // Get all rules rules
      let filteredRules = this.getRules();
      // Get the result of performing the validation promise
      const promise = this.executeValidate(name,currentValue,filteredRules);
      promise
        .catch(e= > e)
        .then((errors: string[] = []) = > {
          if (this.validatePromise === rootPromise) {
            this.validatePromise = null;
            this.errors = errors; // Stores the verification result information
            this.forceUpdate(); // Update the component}});return promise;
    });
    this.validatePromise = rootPromise;
    return rootPromise;
  }
  // Obtain the rules verification result
  public getRules = () = > {
    const { rules = [] } = this.props as any;
    return rules.map(
      (rule:any) = > {
        if (typeof rule === 'function') {
          return rule(this.context);
        }
        returnrule; }); };// Perform rule verification
  executeValidate = (namePath:any,value:any,rules:any) = >{
    let summaryPromise: Promise<string[]>;
    summaryPromise = new Promise(async (resolve, reject) => {
      // If one of the rules fails, you do not need to proceed directly. Return an error result.
      for (let i = 0; i < rules.length; i += 1) {
        const errors = await this.validateRule(namePath, value, rules[i]);
        if (errors.length) {
          reject(errors);
          return;
        }
      }
      resolve([]);
    });
    return summaryPromise;
  }  
  // A method to verify a single pick rule
  validateRule = async (name:any,value:any,rule:any)=>{
    constcloneRule = { ... rule };// Generate a parity object based on name and the parity rule
    const validator = new RawAsyncValidator({
      [name]: [cloneRule],
    });
    let result = [];
    try {
      // Pass the value into the verification object for verification and return the verification result
      await Promise.resolve(validator.validate({ [name]: value }));
    }catch (e) {
      if(e.errors){
        result = e.errors.map((c:any) = >c.message)
      }
    }
    returnresult; }}Copy the code

So far we have completed a simple Form logic module. The code for each section of this article can be viewed on Github, and the corresponding use case can be viewed in the DOSC directory. Click to see the code in this section

Posted online

Published to the NPM

As mentioned earlier, this project uses the Dumi + father-Builder tool, so it is particularly convenient to publish to NPM. After logging in to NPM, you just need to execute the NPM Run Release. Online package address: Lion-form Local items can be used by running the NPM I lion-form command.

Publish the component library documentation

1. Configure.umirc.ts

import { defineConfig } from 'dumi';

let BaseUrl = '/lion-form'; // The path to the warehouse

export default defineConfig({
  // Site description configuration
  mode: 'site'.title: 'lion form'.description: Front-end component development. '.// Package the path configuration
  base: BaseUrl,
  publicPath: BaseUrl + '/'.BaseUrl/xxx.js is generated by importing the address when packing the file
  outputPath: 'docs-dist'.exportStatic: {}, // Output HTML for every other route
  dynamicImport: {}, // Dynamic import
  hash: true.// Add hash configuration to clear cache
  manifest: {
    // Internal publishing system requirements must be configured
    fileName: 'manifest.json',},// Multilingual order
  locales: [['en-US'.'English'],
    ['zh-CN'.'Chinese']],/ / theme
  theme: {
    '@c-primary': '#16c35f',}});Copy the code

After the configuration is complete, run the commandnpm run deployCommand.



2, set upgithub pages 







After the Settings are complete, run the command againnpm run deploy, you can accessOnline component library document address.

conclusion

This article describes how to write a complete React common component library from the steps of engineering construction, source code writing and online distribution. Through the Form component library writing also let us learn:

  • FormComponents,FieldComponents are passed through a globalcontextLinked as a bond, they shareFormStoreData methods in, very similarreduxWorking principle.
  • By putting eachFieldComponent instance registered to globalFormStore, implemented at any location callFieldProperties and methods of the component instance, and whyFielduseclassComponent writing (because function components have no instances).
  • Finally, I used itasync-validatorThe form verification function is realized.

Learning the source code of a good open source library is not a fun process, but the rewards are very big, Dont Worry Be Happy.