preface

This article is based on the form of byte Arco Design. At the end of the article, there are notes for all the code (the code in this paper has been improved by 30% on the original basis, and the quality has been further improved).

Arco design forms refer to Ant forms, but their quality is higher than Ant’s for many reasons. The most important one is that arCO forms have O1 complexity and are based on hash tables when searching for data, while Ant forms have a very high time complexity and need to establish hash tables before searching. At this point, I can’t accept it. I think Ant’s form performance could be much better than it is now. Let’s give an example of why they have this difference.

<FormItem label="Username" field="user.name" >
    <Input placeholder="please enter your username" />
</FormItem>
Copy the code

For example, in the Input field, if you enter “three “, then the form form will get:

{
    user: {
        name: 'Joe'}}Copy the code

Acro form = user.name;

get(this.store, 'user.name')
Copy the code

This get is the lodash method. Let’s see how it works:

var object = { 'a': [{ 'b': { 'c': 3}}}; lodash.get(object,'a[0].b.c'); / / = > 3
Copy the code

Our this.store above can be seen as

{
    user: {
        name: 'Joe'}}Copy the code

So get, which is O1, is very fast to look up.

For Ant, let’s see how the Ant form works:

<FormItem label="Username" name={['user'.'name']} ><Input placeholder="please enter your username" />
</FormItem>
Copy the code

Ant doesn’t have an internal hash table, so you have to create one, something like that

{
    "user_name": "Zhang"
}
Copy the code

Ant can’t create a hash table before arco’s from, because name is named differently. We can see that one is a string and the other is an array, so the array needs to be converted into a hash table first.

Good, to the end of the introduction here, get start business, we write the form first look at the data flow of the overall general design, and then will go through all the code, suggest that we look at the data flow graph even if finished learning, specific code, I want to talk is each line, it is very boring, for interested students to discuss, 😋.

Data flow diagram

First, call useForm to create an intermediary (think of the intermediary pattern or the publish/subscribe pattern), which is the brain of the whole form, called Store in code.

We divided it into two stages: initial rendering and update.

This is how we use components

import { Form, Input, Button, InputNumber } from '@arco-design/web-react';

const FormItem = Form.Item;

function Demo() {
  const [form] = Form.useForm();

  return (
    <Form
      form={form}
      style={{ width: 600 }}
      initialValues={{ name: 'admin'}}onValuesChange={(v, vs) = > {
        console.log(v, vs);
      }}
      onSubmit={(v) => {
        console.log(v);
      }}
    >
      <FormItem label='Username' field='name' rules={[{ required: true}}] >
        <Input placeholder='please enter your username' />
      </FormItem>
        <Button type='primary' htmlType='submit' style={{ marginRight: 24}} >
          Submit
        </Button>
      </FormItem>
    </Form>
}
Copy the code

There are several key components and methods

  • UseForm method, which returns a form that can manipulate the entire form
  • The Form component
  • FormItem components

All right, let’s go ahead and introduce.

First apply colours to a drawing

It doesn’t matter if you don’t understand some of the names in the diagram below, but in a nutshell, this diagram is just creating an instance of the Store class, so we’ll talk about what the Store class is for.

UseForm is called for the first time to create a FormInstance instance. This is a FormInstance instance that uses part of the Store class’s methods.

The Store class has several important properties,

  • Store, which stores the value of the current form
  • RegisterFields, the instance object that holds the FormItem wrapped form element, which means that the Store class controls all forms, and why it does that is because in FormItem component componentDidMount, Put yourself in the registerFields array

A few more important methods:

  • SetFieldsValue, the method of setting a value to a store, exposes formInstance, which is exported by useForm so that external users can get it
  • When setFieldsValue changes the value of store, notify it. When setFieldsValue changes the value of Store, notify it
  • There are other methods like restFieldsValue, resetting the value of the form, getFieldsValue, getting the value of the form, etc., all of which are value operations in store, notify to make changes to the UI

The Form component renders for the first time

Let’s look at the diagram and explain it in detail – one thing I forgot to add to the diagram is that the form assigns initialization data to the intialValue of the store

  • The formProviderCtx can control multiple Form forms, so it contains references to all Form components
  • We pass some methods to the form component, such as onValuesChange and onChange, to listen for events that change the value of the form. Why do these events attach to the Store instance? So if you want to call onValuesChange when the store changes
  • What’s useful from scrollToField, though, is that when a value fails the validate check, it scrolls to the first failed form, which is the implementation function. The Form component then passes the parameters it receives along with the formInstance instance to the FormItem below

formItem

  • The formItem component puts the error and warning displays here, because the formItem component also has a Control component that wraps around form elements, such as the Input element, as shown below:

  • The formItem itself passes the information on the formContext plus an updateFormItem method to form the formItem Context to the Control component below

The Control component

  • The Control component is a class component, and while doing Constructor, let the Store update the initialValue of the Control component, but in practice I really don’t recommend using initialValue. It is better to restore initialValue with setFieldsValue.

  • Register your this with the Store when componentDidMount

  • Shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate shouldupdate I’m not going to talk about this anymore. I can talk about API for a day.

  • Render by default listens for the onChange event on the enclosing single form, validates the event, and passes the value by default

<FormItem label="Username" field="user.name" >
    <Input placeholder="please enter your username" />
</FormItem>
Copy the code

The onChange in the Input component is actually being listened on, so if you change your value, the value in the store changes with it, and the store passes the value of the value into the Input component, so if you do that, it’s a controlled component

<FormItem label="Username" field="user.name" >
    <A />
</FormItem>

const A = (props) = > {
    return  <Input onChange={props.onChange} value={props.value} placeholder="please enter your username" />
}
Copy the code

Okay, now look at the diagram of the update phase

Update the form

We look at update truncation in terms of form element changes, which trigger the onChange event. There are actually 3 events for truncation, the value of the form element changes, the user actively calls setFieldsvalue, and rest, let’s just use onChange, but the rest is the same.

The logic of update truncation is summed up in a nutshell:

  • The data changes in the stored form Store are updated, and the Notify method of the Store class notifies the Control component of the UI changes so that the data and UI are unified.

Detailed code comments

The following code, inclined to boring code explanation, suitable for their own business to write a form component of the students, and once again based on the expansion of business customization function. For example, the form kernel of low code platform can completely use this, why I did not go into details, because this year is to write a set of component library, plus the usual business and study and necessary rest, there is really no time, so I use code comments as explanation.

Component library each component and will publish articles, is a series, the following is the development and packaging cli tools have been written, welcome to the star

Github.com/lio-mengxia…

The file directory looks something like this

  • Form
  • Style folder (style files omitted in this article)
  • Interface folder (ts definition, omitted in this article)
  • Useform.ts (retrieve form hooks)
  • Form.tsx (From component)
  • Form-item.tsx (FormItem component)
  • Form-label. TSX (Label component)
  • Control.tsx (the core component that wraps the form component to keep the form under control)
  • . Other unimportant documents are omitted

Implement useForm

Implementing useForm requires solving several difficulties:

  • How does typescript define object types like this
{
  a: 1.b: 'str'
}
Copy the code

The answer is: Record
,>

  • Typescript defines how to get the types of all the properties of this object, and how to get the types of all the values of this object. Such as:
// For example, there is an interface like this
interface Person {
  name: string;
  age: number;
  gender: string;
}
Copy the code

Remember the keyof operator is used to get the combined type of an interface property

type P = keyof Person; // "name" | "age" | "gender"
Copy the code

And then we’re going to get the value type of the interface

type V = Person[keyof Person] // string | number
Copy the code
  • How to implement singletons in react-hooks
export default function useSingletenHooks(value) {
  const formRef = useRef(null);
  if(! formRef.current) { formRef.current = value; }else {
      return formRef.current
    }
  }
}
Copy the code

Ok, so with that just as a base, let’s write useForm, it should be pretty easy to read


export type IFormData = Record<string.any>;

Formref.current * These instances control the addition, deletion, change, and lookup of the entire form */
export default function useForm<FormData extends IFormData> (form? : FormInstance
       ) :FormInstance<FormData>] {
  const formRef = useRef<FormInstance<FormData>>(form);
  if(! formRef.current) {if (form) {
      formRef.current = form;
    } else{ formRef.current = getFormInstance<FormData>(); }}return [formRef.current];
}

Copy the code

As you can see, the form is a FormInstance, and there’s a getFormInstance method that actually returns a FormInstance, so let’s see what that looks like.

Here’s a quick look at FormInstance. In a nutshell, FormInstance consists of 15 methods, which we’ll explain in a minute.

export type FormInstance<
  FormData = Record<string.any>,
  FieldValue = FormData[keyof FormData],
  FieldKey extends KeyType = keyof FormData
> = Pick<
  Store<FormData, FieldValue, FieldKey>,
  | 'getFieldsValue'
  | 'getFieldValue'
  | 'getFieldError'
  | 'getFieldsError'
  | 'getTouchedFields'
  | 'getFields'
  | 'setFieldValue'
  | 'setFieldsValue'
  | 'setFields'
  | 'resetFields'
  | 'clearFields'
  | 'submit'
  | 'validate'
> & {
  scrollToField: (field: FieldKey, options? : ScrollIntoViewOptions) = > void;
  getInnerMethods: (inner? :boolean) = > InnerMethodsReturnType<FormData, FieldValue, FieldKey>;
};
Copy the code

Here’s a brief description of what these methods do, and we’ll talk more about them later when we talk about Store. I’m going to talk about the Store class.

// External calls set single form field values
setFieldValue(field: FieldKey, value: FieldValue) => void

// External call to set the values of multiple form controls
setFieldsValue (values: DeepPartial<FormData>) => void

// Get the value of a single form
getFieldValue (field: FieldKey) => FieldValue

// Get the value of a single form
// The corresponding errors can also be taken based on the field array passed in
getFieldsError: (fields? : FieldKey[]) = > { [key inFieldKey]? : FieldError<FieldValue>; }// Get a single error message
getFieldError (field: FieldKey): FieldError<FieldValue> | null  

// Get a set of form valuesgetFieldsValue(fields? : FieldKey[]) => Partial<FormData>// This function can perform callback or become promisifyValidate (promisefy package) (fieldsOrCallback: FieldKey [] | ((errors? : ValidateFieldsErrors
       
        , values? : FormData
       ,>) = > void), cb: (errors? : ValidateFieldsErrors
       
        , values? : FormData
       ,>) = > void) :Promise<any> 

// Clear all filed values, note that touch will also be reset
clearFields

// Get all fields that have been manipulated
getTouchedFields () => FieldKey[] 

// Get all the errors * you can also get the errors based on the field array passed in
getFieldsError: (fields? : FieldKey[]) = > { [key inFieldKey]? : FieldError<FieldValue>; }// Get the form value
getFields: Partial<FormData>

// External call to set the values of multiple form controls, as well as error, touch information
setFields(obj: { [field inFieldKey]? : { value? : FieldValue; error? : FieldError<FieldValue>; touched? : boolean; warning? : React.ReactNode; }; }) = >void 

// Reset data to initialValue and reset touch+ resetFields(fieldKeys? : FieldKey | FieldKey[]) =>void  

// Submit the methodSubmit: () = >void 

// This method will be wrapped inside the form component, reassigned, and will use a scroll library to slide through if the DOM ID is known
scrollToField

// Since the form has been wrapped around the component, we need to control some data in the Store instance
// But the methods that control store data are on store instances, so some of these methods must be exposedGetInnerMethods expose the following methods:'registerField', : Collects element instances of the registered FormItem package and removes them when the component is unloaded'innerSetInitialValues'.'innerSetInitialValue'.'innerSetCallbacks'.'innerSetFieldValue'.'innerGetStore'
Copy the code

To realize the getInstance

Ok, with that in mind let’s look at the implementation of the getInstance method, the core of which is the implementation of the Store class.

export function getFormInstance<FormData extends IFormData> () :FormInstance<FormData> {
  const store = new Store<FormData>();
  return {
    getFieldsValue: store.getFieldsValue,
    getFieldValue: store.getFieldValue,
    getFieldError: store.getFieldError,
    getFieldsError: store.getFieldsError,
    getTouchedFields: store.getTouchedFields,
    getFields: store.getFields,
    setFieldValue: store.setFieldValue,
    setFieldsValue: store.setFieldsValue,
    setFields: store.setFields,
    resetFields: store.resetFields,
    clearFields: store.clearFields,
    submit: store.submit,
    validate: store.validate,
    scrollToField: () = > {},
    getInnerMethods: (inner? : boolean): InnerMethodsReturnType<FormData> | {} => {const methods = {} as InnerMethodsReturnType<FormData> | {};
      const InnerMethodsList: InnerMethodsTuple = [
        'registerField'.'innerSetInitialValues'.'innerSetInitialValue'.'innerSetCallbacks'.'innerSetFieldValue'.'innerGetStore',];if (inner) {
        InnerMethodsList.map((key) = > {
          methods[key] = store[key];
        });
      }
      returnmethods; }}; }Copy the code

To achieve the Store class

Before implementation, we need to solve several difficulties:

  • We can use Form and FormItem to do Form functions:
import { Form, Input, Button, InputNumber } from 'UI Component Library ';

const FormItem = Form.Item;

function Demo() {
  const [form] = Form.useForm();

  return (
    <Form
      form={form}
    >
      <FormItem label='Username' field='name'>
        <Input placeholder='please enter your username' />
      </FormItem>
      <FormItem
        label='Age'
        field='age'
      >
        <InputNumber placeholder='please enter your age' />
      </FormItem>
      <FormItem>
        <Button type='primary' htmlType='submit'>
          Submit
        </Button>
        <Button
          onClick={()= > {
            form.resetFields();
          }}
        >
          Reset
        </Button>
        <Button
          onClick={()= > {
            form.setFieldsValue({ name: 'admin', age: 11 });
          }}
        >
          Fill Form
        </Button>
      </FormItem>
    </Form>
  );
}

ReactDOM.render(
  <Demo/>,
  CONTAINER
);
Copy the code

So the elements that are in the FormItem package are actually registered in the Store, like the Input component and the InputNumber component, and there’s a property in the Store called registerFields, which is an array

So in the life cycle of the FormItem componentDidMount, it’s going to pass its own this to this registerFields array, and the Store class has a Store property, which is essentially the form data that we end up with, like if you Input the component and say, So the store property is

{
    name: 'Joe'
}
Copy the code

This means that Stroe is a big boss, and he keeps all the component instances that need to be collected, that is, whoever the FormItem wraps is added to the registerFields (this is not a serious statement, but it makes sense at first).

The Store also contains the data currently stored in the form. Changes in the value of registered components such as Input and InputNumber change the Store property of stored form data in the Store.

Finally, there are methods to reset the Form data, set the Form data, and so on. Some of these methods are exposed to the Form and FormItem components through the formInstance mentioned earlier. But it’s safe to say it’s the biggest Boss at the moment.)

So we look at this class on this cognitive basis and it’s a little less burdensome:

Let’s start with the properties section

class Store<FormData extends IFormData> {
  /** * Collect the instance element instances of the Control element to Control the update of the form element */
  private registerFields: Control<FormData>[] = [];

  /** * Has different properties for formControl touched. As long as the field is modified, it will be stored here. And does not clear */ when formControl is uninstalled
  private touchedFields: Record<string, unknown> = {};

  /** * Store the data in the form */
  private store: Partial<FormData> = {};

  /** * Initializes data */
  private initialValues: Partial<FormData> = {};

  /** * Register some callback functions of the type innerCallbackType (with value changes and submitted events) */
  private callbacks: ICallBack<FormData> = {};

}

Copy the code

React builds the page as a component. There’s no way to pass arguments to an instance of the Store class. It’s just a class, not a component, so you have to pass arguments to an instance of the Store class using a component.

On the Form component, we can pass props, which have properties passed to instances of the Store class, such as the onChange method onValuesChange method.

All of these methods have a feature that the external world wants to be able to receive the internal state of the form, such as the value of the form, the value of the change, and so on, when the form changes or commits important life cycle nodes

Let’s take a look at the TS type of this callback to see which methods are available

type innerCallbackType = 'onValuesChange' | 'onSubmit' | 'onChange' | 'onSubmitFailed' | 'onValidateFail';
Copy the code

Let’s explain what these values mean:

  • OnValuesChange | any form item values change when triggered. The first parameter is the value of the changed form item, and the second parameter is the value of all the form items
  • OnChange | form item values change when triggered. Unlike onValuesChange, it only fires when the user manipulates a form item
  • After the success of the onSubmit | data validation callback event
  • The callback event after onSubmitFailed | data validation fails
  • OnValidateFail | this oversight, didn’t write the official document, estimates were abandoned

Next, there are too many methods in the Store class, and the code is annotated. If you are interested, you can read it. It is really boring.

import get from 'lodash/get';
import setWith from 'lodash/setWith';
import has from 'lodash/has';
import omit from 'lodash/omit';
import { cloneDeep, set, iterativelyGetKeys, isNotEmptyObject, string2Array } from './utils';
import { isArray, isObject } from '.. /_util/is';
import Control from './control';
import { FieldError, ValidateFieldsErrors, FormValidateFn } from './interface/form';
import promisify from './promisify';
import {
  IFormData,
  IFieldValue,
  ICallBack,
  INotifyType,
  IStoreChangeInfo,
} from './interface/store';

class Store<FormData extends IFormData> {
  /** * Trigger the onChange event registered on the form * Note that the value attribute is a string, such as 'name', 'list.1.name'... * /
  private triggerTouchChange(value: Record<string.any>) {
    if (isNotEmptyObject(value)) {
      this.callbacks? .onChange? .(value,this.getFields()); }}/** * Register callbacks, which register value changes and commit events passed to the form */
  public innerSetCallbacks = (values: ICallBack<FormData>) = > {
    this.callbacks = values;
  };

  /** * collect all control fields and remove */ when the component is uninstalled
  public registerField = (item: Control<FormData>) = > {
    this.registerFields.push(item);
    return () = > {
      this.registerFields = this.registerFields.filter((x) = >x ! == item); }; };/** * registerFields: Get the element instance of the fully registered FormItem package * hasField is true, only the control instance of the field attribute */ is returned
  privategetRegisteredFields = (hasField? :boolean): Control<FormData>[] => {
    if (hasField) {
      return this.registerFields.filter(
        (control) = >control.hasFieldProps() && ! control.context? .isFormList ); }return this.registerFields;
  };

  /** * registerFields: get an element instance of a single registered FormItem package * get props. Field === field's control component */
  public getRegisteredField = (field? :string) = > {
    return this.registerFields.find((x) = > x.context.field === field);
  };

  /** * Does two things, one is to mark the changed field as touch * and the second is to notify all formItems to update. There are three types that trigger * setFieldValue: External calls to the setFieldsValue (setFieldValue, etc.) method trigger the update * innerSetValue: Control, such as Input, updates * reset: resets */ triggered by the onChange event
  private notify = (type: INotifyType, info: IStoreChangeInfo<string>) = > {
    if (type= = ='setFieldValue'| | -type= = ='innerSetValue' && !info.ignore)) {
      /** * set field to */
      this._pushTouchField(
        info.changeValues
          ? / * * * info. ChangeValues if is {a: {b: 2}} = > [' a.] * /
            iterativelyGetKeys(info.changeValues)
          : this._getIterativelyKeysByField(info.field)
      );
    }
    this.registerFields.forEach((item) = >{ item.onStoreChange? . (type, {
        ...info,
        current: this.store,
      });
    });
  };

  /** * initialValue initializes the value to the store, but does not synchronize data to the form elements wrapped in the FormItem package */
  public innerSetInitialValues = (values: Partial<FormData>) = > {
    if(! values)return;
    this.initialValues = cloneDeep(values);

    Object.keys(values).forEach((field) = > {
      set(this.store, field, values[field]);
    });
  };

  /** * change InitialValue, change a single value */
  public innerSetInitialValue = (field: string, value: any) = > {
    if(! field)return;
    set(this.initialValues, field, value);
    // Check whether the field is touched during component creation. As long as it has not been operated (it does not exist in touchedFields), it takes effect
    if (!this._inTouchFields(field)) {
      set(this.store, field, get(this.initialValues, field)); }};private _getIterativelyKeysByField(field: string | string[]) {
    if(! field)return [];
    const keys = string2Array(field)
      .map((item) = > iterativelyGetKeys(set({}, item, undefined)))
      .reduce((total, next) = > {
        returntotal.concat(next); } []);return [field, ...keys];
  }

  /** * Check whether the field is touched */
  private _inTouchFields(field? :string) {
    const keys = this._getIterativelyKeysByField(field);
    return keys.some((item) = > has(this.touchedFields, item));
  }

  /** * Remove the field touched */
  private _popTouchField(field? :string | string[]) {
    if (field === undefined) {
      this.touchedFields = {};
    }
    const keys = this._getIterativelyKeysByField(field);
    this.touchedFields = omit(this.touchedFields, keys);
  }

  /** * The touchField will be modified to iterativelyGetKeys */
  private _pushTouchField(field: string | string[]) {
    string2Array(field).forEach((key) = > {
      setWith(this.touchedFields, key, undefined.Object);
    });
  }

  /** * For internal use, updating a value will trigger both onChange and onValuesChange * and force the component corresponding to the field to update including its child */
  public innerSetFieldValue = (
    field: string,
    value: any, options? : { isFormList? :boolean; ignore? :boolean }
  ) = > {
    if(! field)return;
    const prev = cloneDeep(this.store);
    const changeValues = { [field]: value };

    set(this.store, field, value);
    this.triggerValuesChange(changeValues);
    this.triggerTouchChange(changeValues);
    this.notify('innerSetValue', { prev, field, ... options, changeValues }); };/** * get the internal form value. Note that there is no clone store
  public innerGetStore = () = > {
    return this.store;
  };

  /** * retrieve all fields that are operated on, and that are fields on FormItem */
  public getTouchedFields = (): string[] = > {return this.getRegisteredFields(true)
      .filter((item) = > {
        return item.isTouched();
      })
      .map((x) = > x.context.field);
  };

  /** * External calls set a single form field value ** /
  public setFieldValue = (field: string, value: any) = > {
    if(! field)return;
    this.setFields({
      [field]: { value },
    });
  };

  /** * external call to set the value of multiple form controls */
  public setFieldsValue = (values: Record<string.any>) = > {
    if (isObject(values)) {
      const fields = Object.keys(values);
      const obj = {} as {
        [field in string]: { value? : IFieldValue<FormData>; error? : FieldError<IFieldValue<FormData>>; }; }; fields.forEach((field) = > {
        obj[field] = {
          value: values[field],
        };
      });
      this.setFields(obj); }};/** * External call to set the values of multiple form controls, as well as error, touch information. It is possible that the key of obj itself is a path string, such as' A.C.V ', and it is possible that the value is an object, so it needs to process the value * and trigger valuesChange, But it doesn't trigger onChange * where if you pass in waring and errors, it's going to pass that information to the Formitem to display the */
  public setFields = (obj: {
    [field in string]? : { value? :any; error? : FieldError<any>; touched? :boolean; warning? : React.ReactNode; }; }) = > {
    const fields = Object.keys(obj);
    const changeValues: Record<string.any> = {};
    fields.forEach((field) = > {
      const item = obj[field];
      const prev = cloneDeep(this.store);
      if (item) {
        /** * info format * errors? : FieldError
      
       ; * warnings? : React.ReactNode; * touched? : boolean; * /
      
        const info: IStoreChangeInfo<string> ['data'] = {};
        if ('error' in item) {
          info.errors = item.error;
        }
        if ('warning' in item) {
          info.warnings = item.warning;
        }
        if ('touched' in item) {
          info.touched = item.touched;
        }
        if ('value' in item) {
          set(this.store, field, item.value);
          changeValues[field] = item.value;
        }
        this.notify('setFieldValue', {
          data: info,
          prev,
          field,
          changeValues: { [field]: item.value }, }); }});this.triggerValuesChange(changeValues);
  };

  /** * gets a single value ** /
  public getFieldValue = (field: string) = > {
    return get(this.store, field);
  };

  /** * get error information for a single field. * * /
  public getFieldError = (field: string): FieldError<any> | null= > {
    const item = this.getRegisteredField(field);
    return item ? item.getErrors() : null;
  };

  /** * get the incoming fields/all error messages */
  public getFieldsError = (fields? :string[]) = > {
    const errors = {} as { [key in string]? : FieldError<IFieldValue<FormData>> };if (isArray(fields)) {
      fields.map((field) = > {
        const error = this.getFieldError(field);
        if(error) { errors[field] = error; }}); }else {
      this.getRegisteredFields(true).forEach((item) = > {
        if(item.getErrors()) { errors[item.context.field] = item.getErrors(); }}); }return errors;
  };

  /** * get the form value ** /
  public getFields = (): Partial<FormData> => {
    return cloneDeep(this.store);
  };

  /** * retrieves a set of form data, or the form data passed to fields ** /
  publicgetFieldsValue = (fields? :string[]): Partial<FormData> => {
    const values = {};

    if (isArray(fields)) {
      fields.forEach((key) = > {
        set(values, key, this.getFieldValue(key));
      });
      return values;
    }
    this.getRegisteredFields(true).forEach(({ context: { field } }) = > {
      const value = get(this.store, field);
      set(values, field, value);
    });
    return values;
  };

  /** * Simple, just do a few things * set data reset * notify FormItem form data update * trigger valueChange event * update the corresponding form touch attribute */
  public resetFields = (fieldKeys? :string | string[]) = > {
    const prev = cloneDeep(this.store);
    const fields = string2Array(fieldKeys);

    if (fields && isArray(fields)) {
      const changeValues = {};
      /* reset the values */
      fields.forEach((field) = > {
        /* store resets */
        set(this.store, field, get(this.initialValues, field));
        changeValues[field] = get(this.store, field);
      });

      /* Trigger the valueChange event */
      this.triggerValuesChange(changeValues);
      /* Triggers the reset event for each onStoreChange */
      this.notify('reset', { prev, field: fields });
      /* Only the reset event will reset the touch */
      this._popTouchField(fields);
    } else {
      const newValues = {};
      const changeValues = cloneDeep(this.store);
      /* Use initialValue to reset value */
      Object.keys(this.initialValues).forEach((field) = > {
        set(newValues, field, get(this.initialValues, field));
      });
      this.store = newValues;
      this.getRegisteredFields(true).forEach((item) = > {
        const key = item.context.field;
        set(changeValues, key, get(this.store, key));
      });

      this.triggerValuesChange(changeValues);
      this._popTouchField();

      this.notify('reset', { prev, field: Object.keys(changeValues) }); }};/** * validates and retrieves the values and Errors of the form input field, and validates all fields if fields are not set. This promisiFy feeling is too promiscuous */
  public validate: FormValidateFn<FormData> = promisify<FormData>(
    (fieldsOrCallback? : |string[] | ((errors? : ValidateFieldsErrors<FormData>, values? : FormData) =>void), cb? : (errors? : ValidateFieldsErrors<FormData>, values? : FormData) =>void
    ) = > {
      let callback: (errors? : ValidateFieldsErrors
       
        , values? : Partial
        
       ) = > void = () = > {};

      let controlItems = this.getRegisteredFields(true);

      if (isArray(fieldsOrCallback) && fieldsOrCallback.length > 0) {
        controlItems = controlItems.filter((x) = > fieldsOrCallback.indexOf(x.context.field) > -1);
        callback = cb || callback;
      } else if (typeof fieldsOrCallback === 'function') {
        /* check all */ if it is function
        callback = fieldsOrCallback;
      }

      const promises = controlItems.map((x) = > x.validateField());
      /* After verification */
      Promise.all(promises).then((result) = > {
        let errors = {} as ValidateFieldsErrors<FormData>;
        const values = {} as Partial<FormData>;

        result.map((x) = > {
          if(x.error) { errors = { ... errors, ... x.error }; } set(values, x.field, x.value); });/* Error messages are exported to callback and onValidateFail */
        if (Object.keys(errors).length) {
          const { onValidateFail } = this.callbacks; onValidateFail? .(errors); callback? .(errors, cloneDeep(values)); }else{ callback? . (null, cloneDeep(values)); }}); });/** ** commit method, commit time will verify */
  public submit = () = > {
    this.validate((errors, values) = > {
      if(! errors) {const { onSubmit } = this.callbacks; onSubmit? .(values); }else {
        const { onSubmitFailed } = this.callbacks;
        onSubmitFailed?.(errors);
      }
    });
  };

  /** * Clear the value of the form control * it is simple to do a few things * set data reset * notify FormItem form data update * trigger valueChange event * update the corresponding form touch property */
  public clearFields = (fieldKeys? :string | string[]) = > {
    const prev = cloneDeep(this.store);
    const fields = string2Array(fieldKeys);
    if (fields && isArray(fields)) {
      const changeValues = {};
      fields.forEach((field) = > {
        set(this.store, field, undefined);
        changeValues[field] = get(this.store, field);
      });

      this.triggerValuesChange(changeValues);

      this.notify('setFieldValue', { prev, field: fields });
      /** * clearing the value also causes touch to reset */
      this._popTouchField(fields);
    } else {
      const changeValues = {};
      this.store = {};
      this.getRegisteredFields(true).forEach((item) = > {
        const key = item.context.field;
        set(changeValues, key, undefined);
      });

      this.triggerValuesChange(changeValues);
      this._popTouchField();

      this.notify('setFieldValue', {
        prev,
        field: Object.keys(changeValues), }); }}; }export default Store;
Copy the code

Then there is the Form component, which has less code

import React, {
  useImperativeHandle,
  useEffect,
  forwardRef,
  PropsWithChildren,
  useContext,
  useRef,
} from 'react';
import scrollIntoView, { Options as ScrollIntoViewOptions } from 'scroll-into-view-if-needed';
import cs from '.. /_util/classNames';
import useForm from './useForm';
import { FormProps, FormInstance, FieldError, FormContextProps } from './interface/form';
import ConfigProvider, { ConfigContext } from '.. /ConfigProvider';
import { FormContext, FormProviderContext } from './context';
import { isObject } from '.. /_util/is';
import useMergeProps from '.. /_util/hooks/useMergeProps';
import { getFormElementId, getId, ID_SUFFIX } from './utils';
import { IFieldKey, IFormData } from './interface/store';

const defaultProps = {
  layout: 'horizontal' as const.labelCol: { span: 5.offset: 0 },
  labelAlign: 'right' as const.wrapperCol: { span: 19.offset: 0 },
  requiredSymbol: true.wrapper: 'form' as FormProps<FormData>['wrapper'].validateTrigger: 'onChange'};constForm = <FormData extends IFormData>( baseProps: PropsWithChildren<FormProps<FormData>>, ref: React.Ref<FormInstance<FormData>>) => {/** ** Gets the information registered on the root context * each component takes some of the root configuration information from this context */ const CTX = useContext(ConfigContext); /** * The main method is to register formInstance. OnFormValuesChange changes the value of any Form component in the package. Const formProviderCtx = useContext(FormProviderContext); const formProviderCtx = useContext(FormProviderContext); /** * wrap the form dom element reference */ const wrapperRef = useRef<HTMLElement>(null); /** * Set the Store instance generated by useForm to formInstance */ const [formInstance] = useForm<FormData>(baseProps); ComponentDidMount () {/** * useEffect is not used to model componentDidMount (); */ const isMount = useRef< Boolean >(); /* props, */ const props = useMergeProps<FormProps<FormData>>(baseProps, defaultProps, ctx.componentConfig?.form); const { layout, labelCol, wrapperCol, wrapper: Wrapper, id, requiredSymbol, labelAlign, disabled, colon, className, validateTrigger, size: formSize, } = props; const prefixCls = ctx.getPrefixCls('form'); const size = formSize || ctx.size; const innerMethods = formInstance.getInnerMethods(true); /** * const contextProps (); /** * const contextProps (); /** * const contextProps (); FormContextProps = { requiredSymbol, labelAlign, disabled, colon, labelCol, wrapperCol, layout, store: formInstance, prefixCls, validateTrigger, getFormElementId: (field: string) => getId({ getFormElementId, id, field, ID_SUFFIX }), }; if (! isMount.current) { innerMethods.innerSetInitialValues(props.initialValues); } useEffect(() => { isMount.current = true; } []); useEffect(() => { let unregister; if (formProviderCtx.register) { unregister = formProviderCtx.register(props.id, formInstance); } return unregister; }, [props.id, formInstance]); useImperativeHandle(ref, () => { return formInstance; }); / / scroll to the target form field position formInstance scrollToField = (field: IFieldKey < FormData >, the options? Const node = wrapperref.current; const node = wrapperref.current; /** * const id = props. Id; if (! node) { return; } /** * formItem puts this id on the DOM, */ const fieldNode = node.querySelector(' #${getId({getFormElementId, id, field, ID_SUFFIX})} '); fieldNode && scrollIntoView(fieldNode, { behavior: 'smooth', block: 'nearest', scrollMode: 'if-needed', ... options, }); }; /** * the callback attribute is assigned to the store instance, which passes the custom method to the Store. OnValuesChange * onChange: onChange * onValidateFail: onSubmitFailed: onSubmit After the success of the data validation callback event. * / innerMethods innerSetCallbacks ({onValuesChange: (value, values) => { props.onValuesChange && props.onValuesChange(value, values); formProviderCtx.onFormValuesChange && formProviderCtx.onFormValuesChange(props.id, value); }, onChange: props.onChange, onValidateFail: (errors: { [key in string]: FieldError<any> }) => { if (props.scrollToFirstError) { const options = isObject(props.scrollToFirstError) ? props.scrollToFirstError : {}; formInstance.scrollToField(Object.keys(errors)[0], options); } }, onSubmitFailed: props.onSubmitFailed, onSubmit: (values) => { props.onSubmit && props.onSubmit(values); formProviderCtx.onFormSubmit && formProviderCtx.onFormSubmit(props.id, values); }}); return ( <ConfigProvider {... ctx} size={size}> <FormContext.Provider value={contextProps}> <Wrapper ref={wrapperRef} {... / / className={cs(prefixCls, '${prefixCls}-${layout}', `${prefixCls}-size-${size}`, className )} style={props.style} onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); /* Call store submit */ formInstance. Submit (); }} id={id} > {props.children} </Wrapper> </FormContext.Provider> </ConfigProvider> ); }; const FormComponent = forwardRef(Form); FormComponent.displayName = 'Form'; export default FormComponent as <FormData extends IFormData>( props: React.PropsWithChildren<FormProps<FormData>> & { ref? : React.Ref<FormInstance<FormData>>; } ) => React.ReactElement;Copy the code

FormItem Component code comment

import React, {
  cloneElement,
  ReactElement,
  forwardRef,
  useContext,
  PropsWithChildren,
  useState,
  useEffect,
  useMemo,
  ReactNode,
  useRef,
} from 'react';
import { CSSTransition } from 'react-transition-group';
import cs from '.. /_util/classNames';
import { isArray, isFunction, isUndefined, isObject } from '.. /_util/is';
import Grid from '.. /Grid';
import { FormItemProps, FieldError, VALIDATE_STATUS } from './interface/form';
import Control from './control';
import { FormItemContext, FormContext } from './context';
import FormItemLabel from './form-label';
import { IFormData } from './interface/store';

const Row = Grid.Row;
const Col = Grid.Col;

interface FormItemTipProps extends Pick<FormItemProps, 'prefixCls' | 'help'> {
  errors: FieldError[];
  warnings: ReactNode[];
}

/** * Error message */
const FormItemTip: React.FC<FormItemTipProps> = ({ prefixCls, help, errors: propsErrors, warnings = [], }) = > {
  /** * Error message aggregation */
  const errorTip = propsErrors.map((item, i) = > {
    if (item) {
      return <div key={i}>{item.message}</div>; }});const warningTip = [];
  /** * waring information aggregation */
  warnings.map((item, i) = > {
    warningTip.push(
      <div key={i} className={` ${prefixCls}-message-help-warning`} >
        {item}
      </div>
    );
  });
  /** * isHelpTip is true */ if a customized checkscript exists or warnings exists
  constisHelpTip = ! isUndefined(help) || !! warningTip.length;/** * The display condition is: custom copy, warning, or errors */
  constvisible = isHelpTip || !! errorTip.length;return (
    visible && (
      <CSSTransition in={visible} appear classNames="formblink" timeout={300} unmountOnExit>
        <div
          className={cs(` ${prefixCls}-message`, ${{[`prefixCls}-message-help`]: isHelpTip,})} >{! isUndefined(help) ? ( help ) : (<>
              {errorTip.length > 0 && errorTip}
              {warningTip.length > 0 && warningTip}
            </>
          )}
        </div>
      </CSSTransition>
    )
  );
};

const Item = <FormData extends IFormData>(
  props: PropsWithChildren<FormItemProps<FormData>>,
  ref: React.Ref<typeof Row>
) => {
  /**
   * formItem的context只是比formContext多了一个updateFormItem
   */
  const topFormItemContext = useContext(FormItemContext);
  const [errors, setErrors] = useState<{
    [key: string]: FieldError;
  }>(null);
  const [warnings, setWarnings] = useState<{
    [key: string]: ReactNode[];
  }>(null);

  /**
   * 获取外部formContext的传入
   */
  const formContext = useContext(FormContext);
  const prefixCls = formContext.prefixCls;
  /* 收敛layout属性 */
  const formLayout = props.layout || formContext.layout;
  /* 收敛label布局属性 */
  const labelAlign = props.labelAlign || formContext.labelAlign;
  /* 收敛disabled属性 */
  const disabled = 'disabled' in props ? props.disabled : formContext.disabled;
  const errorInfo = errors ? Object.values(errors) : [];
  const warningInfo = warnings
    ? Object.values(warnings).reduce((total, next) => total.concat(next), [])
    : [];

  /**
   * rest还有
   * style initialValue field labelCol wrapperCol colon disabled rules trigger
   * triggerPropName getValueFromEvent validateTrigger noStyle required hasFeedback help
   * normalize formatter shouldUpdate labelAlign requiredSymbol
   */
  const { label, extra, className, style, validateStatus, hidden } = props;
  /**
   * 是否这个组件已经卸载了
   */
  const isDestroyed = useRef(false);

  /**
   * 把error和warning数据同步到UI
   */
  const updateInnerFormItem = (
    field: string,
    params: {
      errors?: FieldError;
      warnings?: ReactNode[];
    } = {}
  ) => {
    if (isDestroyed.current) {
      return;
    }
    const { errors, warnings } = params || {};

    setErrors((preErrors) => {
      const newErrors = { ...(preErrors || {}) };
      if (errors) {
        newErrors[field] = errors;
      } else {
        delete newErrors[field];
      }
      return newErrors;
    });
    setWarnings((preWarnings) => {
      const newVal = { ...(preWarnings || {}) };
      if (warnings && warnings.length) {
        newVal[field] = warnings;
      } else {
        delete newVal[field];
      }
      return newVal;
    });
  };

  const updateFormItem =
    isObject(props.noStyle) && props.noStyle.showErrorTip && topFormItemContext.updateFormItem
      ? topFormItemContext.updateFormItem
      : updateInnerFormItem;

  useEffect(() => {
    return () => {
      isDestroyed.current = true;
      setErrors(null);
      setWarnings(null);
    };
  }, []);

  /**
   * 传给control的数据
   */
  const contextProps = {
    ...formContext,
    prefixCls,
    updateFormItem,
    disabled,
    field: isArray(props.children) ? undefined : props.field,
    shouldUpdate: props.shouldUpdate,
    trigger: props.trigger,
    normalize: props.normalize,
    getValueFromEvent: props.getValueFromEvent,
    children: props.children,
    rules: props.rules,
    validateTrigger: props.validateTrigger || formContext.validateTrigger || 'onChange',
    triggerPropName: props.triggerPropName,
    validateStatus: props.validateStatus,
    formatter: props.formatter,
    noStyle: props.noStyle,
    isFormList: props.isFormList,
    hasFeedback: props.hasFeedback,
    initialValue: props.initialValue,
  };
  const labelClassNames = cs(`${prefixCls}-label-item`, {
    [`${prefixCls}-label-item-left`]: labelAlign === 'left',
  });

  /**
   * 收敛状态 自定义validateStatus必须跟feedback一起用在control的右边才有icon
   */
  const itemStatus = useMemo(() => {
    if (validateStatus) {
      return validateStatus;
    }
    if (errorInfo.length) {
      return VALIDATE_STATUS.error;
    }
    if (warningInfo.length) {
      return VALIDATE_STATUS.warning;
    }
    return undefined;
  }, [errors, warnings, validateStatus]);

  const hasHelp = useMemo(() => {
    return !isUndefined(props.help) || warningInfo.length > 0;
  }, [props.help, warnings]);

  const classNames = cs(
    // width: 100%;
    // margin-bottom: 20px;
    // display: flex;
    // justify-content: flex-start;
    // align-items: flex-start;
    `${prefixCls}-item`,
    {
      /* margin-bottom: 0 */
      [`${prefixCls}-item-error`]:
        hasHelp || (!validateStatus && itemStatus === VALIDATE_STATUS.error),
      // 让下面的control组件定义backgroundcolor
      [`${prefixCls}-item-status-${itemStatus}`]: itemStatus,
      // 无样式
      [`${prefixCls}-item-has-help`]: hasHelp,
      // display: none
      [`${prefixCls}-item-hidden`]: hidden,
      /* 让control下的组件定义 padding-right: 28px; */
      [`${prefixCls}-item-has-feedback`]: itemStatus && props.hasFeedback,
    },
    /* 无样式 */
    `${prefixCls}-layout-${formLayout}`,
    className
  );

  const cloneElementWithDisabled = () => {
    const { field, children } = props;
    if (isFunction(children)) {
      return <Control {...(field ? { key: field } : {})}>{(...rest) => children(...rest)}</Control>;
    }
    if (isArray(children)) {
      const childrenDom = React.Children.map(children, (child, i) => {
        const key = (isObject(child) && (child as ReactElement).key) || i;
        return isObject(child) ? cloneElement(child as ReactElement, { key }) : child;
      });
      return <Control>{childrenDom}</Control>;
    }
    if (React.Children.count(children) === 1) {
      if (field) {
        return <Control key={field}>{children}</Control>;
      }
      if (isObject(children)) {
        return <Control>{children}</Control>;
      }
    }

    return children;
  };
  return (
    <FormItemContext.Provider value={contextProps}>
      {props.noStyle ? (
        cloneElementWithDisabled()
      ) : (
        <Row ref={ref} className={classNames} div={formLayout !== 'horizontal'} style={style}>
          {label ? (
            <Col
              {...(props.labelCol || formContext.labelCol)}
              className={cs(
                labelClassNames,
                props.labelCol?.className,
                formContext.labelCol?.className,
                {
                  [`${prefixCls}-label-item-flex`]: !props.labelCol && !formContext.labelCol,
                }
              )}
            >
              <FormItemLabel
                htmlFor={props.field && formContext.getFormElementId(props.field)}
                label={label}
                prefix={prefixCls}
                requiredSymbol={
                  'requiredSymbol' in props ? props.requiredSymbol : formContext.requiredSymbol
                }
                required={props.required}
                rules={props.rules}
                showColon={'colon' in props ? props.colon : formContext.colon}
              />
            </Col>
          ) : null}
          <Col
            className={cs(`${prefixCls}-item-wrapper`, {
              [`${prefixCls}-item-wrapper-flex`]: !props.wrapperCol && !formContext.wrapperCol,
            })}
            {...(props.wrapperCol || formContext.wrapperCol)}
          >
            {cloneElementWithDisabled()}
            <FormItemTip
              prefixCls={prefixCls}
              help={props.help}
              errors={errorInfo}
              warnings={warningInfo}
            />
            {extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
          </Col>
        </Row>
      )}
    </FormItemContext.Provider>
  );
};

const ItemComponent = forwardRef(Item);

ItemComponent.defaultProps = {
  trigger: 'onChange',
  triggerPropName: 'value',
};

ItemComponent.displayName = 'FormItem';

export default ItemComponent as <FormData = any>(
  props: React.PropsWithChildren<FormItemProps<FormData>> & {
    ref?: React.Ref<typeof Row['prototype']>;
  }
) => React.ReactElement;
Copy the code

Control component code comments

import React, { Component, ReactElement } from 'react';
import isEqualWith from 'lodash/isEqualWith';
import has from 'lodash/has';
import set from 'lodash/set';
import get from 'lodash/get';
import setWith from 'lodash/setWith';
import { FormControlProps, FieldError, FormItemContextProps } from './interface/form';
import { FormItemContext } from './context';
import { isArray, isFunction } from '.. /_util/is';
import warn from '.. /_util/warning';
import IconExclamationCircleFill from '.. /.. /icon/react-icon/IconExclamationCircleFill';
import IconCloseCircleFill from '.. /.. /icon/react-icon/IconCloseCircleFill';
import IconCheckCircleFill from '.. /.. /icon/react-icon/IconCheckCircleFill';
import IconLoading from '.. /.. /icon/react-icon/IconLoading';
import { isSyntheticEvent, schemaValidate } from './utils';
import {
  IFormData,
  IFieldValue,
  IFieldKey,
  IStoreChangeInfo,
  INotifyType,
} from './interface/store';

/** * Whether to update the table UI path on the existing filed control */
function isFieldMath(field, fields) {
  const fieldObj = setWith({}, field, undefined.Object);

  return fields.some((item) = > has(fieldObj, item));
}

export default class Control<FormData extends IFormData> extends Component<
  FormControlProps<FormData>
> {
  /** ** ** ** /
  static isFormControl = true;

  /** * references the context passed above, which is the */ for FormItem
  static contextType = FormItemContext;

  context: FormItemContextProps<FormData>;

  /** * Errors and the following warning are passed by setFieldsValue, and the UI is updated with updateFormItem */
  private errors: FieldError<IFieldValue<FormData>> = null;

  private warnings: React.ReactNode[] = null;

  private isDestroyed = false;

  private touched: boolean;

  /** * unmount this control */
  private removeRegisterField: () = > void;

  constructor(props: FormControlProps<FormData>, context: FormItemContextProps<FormData>) {
    super(props);
    /** * setInitialValue */
    if(context.initialValue ! = =undefined && this.hasFieldProps(context)) {
      const innerMethods = context.store.getInnerMethods(true); innerMethods.innerSetInitialValue(context.field, context.initialValue); }}/** * Register yourself on stroe */
  componentDidMount() {
    const { store } = this.context;
    if (store) {
      const innerMethods = store.getInnerMethods(true);
      this.removeRegisterField = innerMethods.registerField(this); }}/** * delete reference */ from store
  componentWillUnmount() {
    this.removeRegisterField? . ();this.removeRegisterField = null;

    /** * Delete errors and warnings */
    const { updateFormItem } = this.context; updateFormItem? . (this.context.field as string, { errors: null.warnings: null });
    this.isDestroyed = true;
  }

  getErrors = (): FieldError<IFieldValue<FormData>> | null= > {
    return this.errors;
  };

  isTouched = (): boolean= > {
    return this.touched;
  };

  publichasFieldProps = (context? : FormItemContextProps<FormData>):boolean= > {
    return!!!!! (context ||this.context).field;
  };

  /** * force render, map store data to the latest UI, and update error and Warnings UI */
  private updateFormItem = () = > {
    if (this.isDestroyed) return;
    this.forceUpdate();
    const { updateFormItem } = this.context;
    updateFormItem &&
      updateFormItem(this.context.field as string, {
        errors: this.errors,
        warnings: this.warnings,
      });
  };

  /** * When store notify is triggered, the FormItem package is updated. * Type is rest: Touched Errors warning Resets the component to rerender because a new store value is reflected on the form UI * Type is innerSetValue, which is triggered when the form itself changes, such as an onChange event, The result is to change touch to true, and the component rerender * type to setFieldValue, which is triggered by an external call to setFieldValue
  public onStoreChange = (type: INotifyType, info: IStoreChangeInfo<string> & { current: any }) = > {
    /** * fields to array format */
    const fields = isArray(info.field) ? info.field : [info.field];
    const { field, shouldUpdate } = this.context;

    // isInner: the value is changed by innerSetValue
    // If your FromItem attribute is shouldUpdate this is checked and executed
    const shouldUpdateItem = (extra? : { isInner? :boolean; isFormList? :boolean }) = > {
      if (shouldUpdate) {
        let shouldRender = false;
        if (isFunction(shouldUpdate)) {
          shouldRender = shouldUpdate(info.prev, info.current, {
            field: info.field, ... extra, }); }else{ shouldRender = ! isEqualWith(info.prev, info.current); }if (shouldRender) {
          this.updateFormItem(); }}};switch (type) {
      case 'reset':
        this.touched = false;
        this.errors = null;
        this.warnings = null;
        this.updateFormItem();
        break;
      case 'innerSetValue':
        if (isFieldMath(field, fields)) {
          this.touched = true;
          this.updateFormItem();
          return;
        }
        shouldUpdateItem({
          isInner: true.isFormList: info.isFormList,
        });
        break;
      case 'setFieldValue':
        if (isFieldMath(field, fields)) {
          this.touched = true;
          if (info.data && 'touched' in info.data) {
            this.touched = info.data.touched;
          }
          if (info.data && 'warnings' in info.data) {
            this.warnings = [].concat(info.data.warnings);
          }
          if (info.data && 'errors' in info.data) {
            this.errors = info.data.errors;
          } else if(! isEqualWith(get(info.prev, field), get(info.current, field))) {this.errors = null;
          }
          this.updateFormItem();
          return;
        }
        shouldUpdateItem();
        break;
      default:
        break; }};/** * The form itself changes in value, such as the onChange event that triggers an internal setValue */
  innerSetFieldValue = (field: string, value: IFieldValue<FormData>) = > {
    if(! field)return;
    const { store } = this.context;
    const methods = store.getInnerMethods(true);
    methods.innerSetFieldValue(field, value);

    const changedValue = {} as Partial<FormData>;
    set(changedValue, field, value);
  };

  Value => getValueFromEvent => Normalize => children? .props? .[trigger] */
  handleTrigger = (_value, ... args) = > {
    const { store, field, trigger, normalize, getValueFromEvent } = this.context;
    constvalue = isFunction(getValueFromEvent) ? getValueFromEvent(_value, ... args) : _value;const children = this.context.children as ReactElement;
    let normalizeValue = value;
    // break if value is instance of SyntheticEvent, 'cos value is missing
    if (isSyntheticEvent(value)) {
      warn(
        true.'changed value missed, please check whether extra elements is outta input/select controled by Form.Item'
      );
      value.stopPropagation();
      return;
    }

    if (typeof normalize === 'function') { normalizeValue = normalize(value, store.getFieldValue(field), { ... store.getFieldsValue(), }); }this.touched = true;
    this.innerSetFieldValue(field, normalizeValue);

    this.validateField(trigger); children? .props? .[trigger]? .(normalizeValue, ... args); };/** * First filter out all rules that trigger the current event and rules have an indication of the event change. Then use schemaValidate to verify this, and return the result to this.errors and warnings */validateField = ( triggerType? :string) :PromiseThe < {error: FieldError<IFieldValue<FormData>> | null; value: IFieldValue<FormData>; field: IFieldKey<FormData>; } > = > {const { store, field, rules, validateTrigger } = this.context;
    const value = store.getFieldValue(field);
    const_rules = ! triggerType ? rules : (rules || []).filter((rule) = > {
          const triggers = [].concat(rule.validateTrigger || validateTrigger);
          return triggers.indexOf(triggerType) > -1;
        });
    if (_rules && _rules.length && field) {
      return schemaValidate(field, value, _rules).then(({ error, warning }) = > {
        this.errors = error ? error[field] : null;
        this.warnings = warning || null;
        this.updateFormItem();
        return Promise.resolve({ error, value, field });
      });
    }
    if (this.errors) {
      this.errors = null;
      this.warnings = null;
      this.updateFormItem();
    }
    return Promise.resolve({ error: null, value, field });
  };

  /** * Collect the rules validateTrigger field */
  getValidateTrigger(): string[] {
    const _validateTrigger = this.context.validateTrigger;
    const rules = this.context.rules || [];

    const result = rules.reduce((acc, curr) = > {
      acc.push(curr.validateTrigger || _validateTrigger);
      returnacc; } []);return Array.from(new Set(result));
  }

  /** * bind the validate-related event to the children event. The default onChange binding to children * binds the disabled property to children * reprocesses the value fetched from store with formatter to the form */
  renderControl(children: React.ReactNode, id) {
    // Trigger context defaults to 'onChange',
    // triggerPropName context defaults to 'value',
    const { field, trigger, triggerPropName, validateStatus, formatter } = this.context;
    const { store, disabled } = this.context;
    const child = React.Children.only(children) as ReactElement;
    const childProps: any = {
      id,
    };

    this.getValidateTrigger().forEach((validateTriggerName) = > {
      childProps[validateTriggerName] = (e) = > {
        this.validateField(validateTriggerName); child.props? .[validateTriggerName](e); }; }); childProps[trigger] =this.handleTrigger;

    if(disabled ! = =undefined) {
      childProps.disabled = disabled;
    }
    let _value = store.getFieldValue(field);

    if (isFunction(formatter)) {
      _value = formatter(_value);
    }

    childProps[triggerPropName] = _value;
    if(! validateStatus &&this.errors) {
      childProps.error = true;
    }

    return React.cloneElement(child, childProps);
  }

  getChild = () = > {
    const { children } = this.context;
    const { store } = this.context;
    if (isFunction(children)) {
      returnchildren(store.getFields(), { ... store, }); }return children;
  };

  render() {
    const { noStyle, field, isFormList, hasFeedback } = this.context;
    const validateStatus = this.context.validateStatus || (this.errors ? 'error' : ' ');
    const { prefixCls, getFormElementId } = this.context;
    let child = this.getChild();
    const id = this.hasFieldProps() ? getFormElementId(field) : undefined;
    if (this.hasFieldProps() && ! isFormList && React.Children.count(child) ===1) {
      child = this.renderControl(child, id);
    }

    if (noStyle) {
      return child;
    }
    return (
      <div className={` ${prefixCls}-item-control-wrapper`} >
        <div className={` ${prefixCls}-item-control`} id={id}>
          <div className={` ${prefixCls}-item-control-children`} >
            {child}

            {validateStatus && hasFeedback && (
              <div className={` ${prefixCls}-item-feedback`} >
                {validateStatus === 'warning' && <IconExclamationCircleFill />}
                {validateStatus === 'success' && <IconCheckCircleFill />}
                {validateStatus === 'error' && <IconCloseCircleFill />}
                {validateStatus === 'validating' && <IconLoading />}
              </div>
            )}
          </div>
        </div>
      </div>); }}Copy the code