Github address: github.com/reeli/react…

The words written in the front

What you need to know before reading this article:

  • React
  • RxJS (at least you need to know what Subject is)

background

Forms are arguably one of the biggest challenges in Web development. Compared to normal components, forms have the following characteristics:

  1. More user interaction. This means that a number of custom components, such as DataPicker, Upload, AutoComplete, and so on, may be required.
  2. Frequent state changes. Each time a user enters a value, the application state may change, requiring form elements to be updated or an error message displayed.
  3. Form validation, which verifies the validity of user input data. Form validation can take many forms, such as validation as you type, validation after you lose focus, validation before you submit the form, and so on.
  4. Asynchronous network communication.When both user input and asynchronous network communication exist, there is more to consider. AutoComplete, for example, requires asynchronous retrieval of data based on user input. If the user initiates a request for each input, it will be a waste of resources. Because every input isasynchronousIf the user input data twice in a row, there may be a “last to first” problem.

Because of these characteristics, form development becomes difficult. In the following chapters, we will combine RxJS and Form to help us solve these problems better.

HTML Form

Before implementing our own Form component, let’s take a look at native HTML Forms.

Save form state

For a Form component, information about all Form elements (value, validity, etc.) needs to be stored, and HTML forms are no exception. So, where do HTML forms store Form state? How do I get form element information?

There are mainly the following methods:

  1. document.formsWill return all<form>Form node.
  2. Htmlformelement. elements returns all form elements.
  3. Event.target. elements also fetches all form elements.
document.forms[0].elements[0].value; // Get the value of the first form element in the first form

const form = document.querySelector("form");
form.elements[0].value; 

form.addEventListener('submit'.function(event) {
  console.log(event.target.elements[0].value);
});
Copy the code

Validation

There are two types of form verification:

  1. Built-in form validation. This is triggered automatically by default when the form is submitted. By setting thenovalidateProperty to turn off automatic validation for the browser.
  2. JavaScript validation.
<form novalidate>
  <input name='username' required/>
  <input name='password' type='password' required minlength="6" maxlength="6"/>
  <input name='email' type='email'/>
  <input type='submit' value='submit'/>
</form>
Copy the code

Existing problems

  • Customization is hard. For example, Inline Validation is not supported. Only submit validates the form, and the error message style cannot be customized.
  • Difficult to deal with complex scenarios. For example, nesting of form elements.
  • The Input component does not behave uniformly, making it difficult to get the value of the form element. For example, checkbox and multiple select cannot take value directly, and additional conversion is required.
var $form = document.querySelector('form');

function getFormValues(form) {
  var values = {};
  var elements = form.elements; // elemtns is an array-like object

  for (var i = 0; i < elements.length; i++) {
    var input = elements[i];
    if (input.name) {
      switch (input.type.toLowerCase()) {
        case 'checkbox':
          if (input.checked) {
            values[input.name] = input.checked;
          }
          break;
        case 'select-multiple':
          values[input.name] = values[input.name] || [];
          for (var j = 0; j < input.length; j++) {
            if(input[j].selected) { values[input.name].push(input[j].value); }}break;
        default:
          values[input.name] = input.value;
          break; }}}return values;
}

$form.addEventListener('submit'.function(event) {
  event.preventDefault();
  getFormValues(event.target);
  console.log(event.target.elements);
  console.log(getFormValues(event.target));
});
Copy the code

React Rx Form

Interested students can first go to see the source github.com/reeli/react…

The React with RxJS

RxJS is a very powerful data management tool, but it doesn’t have user interface rendering capabilities, whereas React is particularly good at handling interfaces. Why not combine their strengths? React and RxJS to solve our Form problems. Now that their strengths are known, the division of labor is fairly clear:

RxJS manages state and React renders the interface.

Design ideas

Unlike Redux forms, we do not store the Form’s state in the Store, but directly in the

component. The
component then uses RxJS to inform each
of the data. The
component then decides whether it needs to update the UI based on the data. If it needs to update the UI, it calls setState, otherwise it does nothing.

For example, suppose you have three fields in a Form (see below) and only the value of FieldA changes in order not to

shouldComponentUpdate()

/ / pseudo code<Form>
    <FieldA/>
    <FieldB/>
    <FieldC/>
</Form>
Copy the code

RxJS keeps the granularity of component updates to a minimum. In other words,
re-render that really needs re-render and components that don’t need re-render don’t need to be re-rendered.

The core is the Subject

The following two problems can be summarized from the above design ideas:

  1. Form and Field have a one-to-many relationship, and the status of the Form needs to be notified to multiple fields.
  2. Field needs to modify the state of the component based on the data.

The first problem is that you need an Observable that supports multicast. The second problem requires an Observer capability. In RxJS, a Subject is an Observable and Observer that implements multicast. Therefore, Subject is heavily used when implementing forms.

FormState data structure

The Form component also needs a State to hold the State of all fields. This State is called the Form State.

So how do you define the structure of formState?

In the earliest versions, the formState structure looked like this:

interface IFormState {
  [fieldName: string]: { dirty? :boolean; touched? :boolean; visited? :boolean; error? : TError; value:string;
  };
}
Copy the code

FormState is an object that takes fieldName as its key and an object holding the Field state as its value.

Doesn’t look wrong, does it?

But…

The formState structure ended up like this:

interface IFormState {
  fields: {
    [fieldName: string]: { dirty? :boolean; touched? :boolean; visited? :boolean; error? :string | undefined;
    };
  };
  values: {
    [fieldName: string] :any;
  };
}

Copy the code

Note: Field does not contain filed Value, but only some state information of field. Values contains only field values.

Why??

In fact, both data structures work when implementing the most basic Form and Field components.

So what’s the problem?

Just to keep things in perspective, all you need to know is what the data structure for formState looks like.

The data flow

To better understand data flow, let’s look at a simple example. We have a Form component that contains a Field component and a Text Input inside the Field component. The data flow might look something like this:

  1. The user enters a character in the input box.
  2. The onChange event Input will be triggered.
  3. Field’s onChange Action will be dispatched.
  4. Modify formState according to Field’s onChange Action.
  5. Field observers are notified when the Form State is updated.
  6. The observer of the Field picks up the State of the current Field, setState if it finds an update, and does nothing if there is no update.
  7. SetState will rerender the Field, and the new Field Value will be notified to the Input.

Core components

First, we need to create two basic components, a Field component and a Form component.

The Field component

The Field component is the middle layer that connects the Form component to the Form element. It serves to simplify the Input component’s responsibilities. With it, Input just needs to be displayed and you don’t need to worry about complicated logic (validate/ Normalize, etc.). Moreover, the Field layer abstraction is important because the Input component can be used not only in the Form component but also outside of the Form component (some of which may not require logic like validate).

  • Interception and transformation. The format/parse the normalize.
  • Form validation. By referring to HTML Form validation, we can place Validation on the Field component and combine validation rules to suit different requirements.
  • Trigger a change in field state (e.g. touched, visited)
  • Provide the required information to the child components. Error, touched, visited… , and callback functions for form element binding events (onChange, onBlur…). .

Take advantage of RxJS features to control Field component updates and reduce unnecessary rerender.

Communicate with the Form. The Form needs to be notified when the Field state changes. The Field also needs to be notified when the state of a Field changes in a Form.

The Form component

  • Manage form state. The Form component provides the Form state to the Field and notifies the Form when the Field changes.
  • Provide formValues.
  • Block form submission if form validation fails.

Notifies Field of every Form State change. A formSubject$is created in the Form, each change in Form State sends a data to the formSubject$, and each Field is registered as an observer of the formSubject$. This means that the Field knows every change in the Form State and can therefore decide to update it when appropriate. Notify Field when the FormAction changes. For example, startSubmit.

Communication between components

  1. Form communicates with Field.

    Context is primarily used for cross-level component communication. In real development, the Form and Field may cross levels, so we need to use Context to ensure that the Form and Field communicate. The Form provides its instance method and Form State to the Field through the context.

  2. Field communicates with Form.

    The Form component provides a Dispatch method to the Field component to communicate with the Form. The state and values of all fields are managed by the Form. If you want to update the state or value of a Field, you must dispatch the corresponding action.

  3. Form elements communicate with fields

    Form elements communicate with fields primarily through callback functions. Field provides onChange, onBlur, and other callback functions to form elements.

Interface design

Simplicity and clarity are important for interface design. So the Field retains only the necessary attributes, and instead of passing the other attributes required by the form elements through the Field, the form elements define them themselves.

With Child Render, the corresponding states and methods are provided to the Child components, making the structure and hierarchy clearer.

Field:

type TValidator = (value: string | boolean) = > string | undefined;

interface IFieldProps {
  children: (props: IFieldInnerProps) = > React.ReactNode;
  name: string; defaultValue? :any; validate? : TValidator | TValidator[]; }Copy the code

Form:

interface IRxFormProps {
  children: (props: IRxFormInnerProps) = >React.ReactNode; initialValues? : { [fieldName:string] :any; }}Copy the code

At this point, a very basic Form is complete. We will then extend it to accommodate more complex business scenarios.

Enhance

FieldArray

FieldArray is primarily used to render multiple groups of Fields.

Going back to our earlier question, why split the formState structure into fileds and values?

The problem was FieldArray,

  • The initial length is determined by initLength or formValues.
  • FormState update as a whole.

FormValues

With RxJS, we keep the granularity of Field updates to a minimum, meaning that if the Value of a Field changes, it will not cause the Form component and other Feild components rerender.

Since a Field can only sense its own value changes, then the question arises: how to realize the linkage between fields?

The FormValues component was born.

Whenever a formValues change, the formValues component notifies the child component of the new formValues. This means that if you use the FormValues component, every change to FormValues will result in the FormValues component and its child rerender, so extensive use of the FormValues component is not recommended as it may cause performance problems.

In general, when using FormValues, it is best to put it in a place with minimal impact. That is, rerender as few components as possible when formValues change.

In the following code, the display of FieldB depends on the value of FieldA, so you just need to apply FormValues to FieldA and FieldB.

<FormValues>
    {({ formValues, updateFormValues }) => (
        <>
            <FieldA name="A" />{!!!!! formValues.A &&<FieldB name="B" />}
        </>
    )}
</FormValues>
Copy the code

FormSection

The FormSection is used primarily to group a set of Fields that can be reused across multiple forms. This is done by prefixing the names of Field and FieldArray.

How do you prefix the names of Field and FieldArray?

The first thing THAT comes to mind is to get the name of the child component through react. Children and concatenate it with the name of the FormSection.

However, the FormSection and Field may not be parent-child! The Field component can also be split into a separate component. Therefore, there is the problem of cross-level component communication.

That’s right! We’re still going to use context for cross-level component communication. We need to get the context value from FormConsumer and provide the prefix to the Consumer through the Provider. The value that Field/FieldArray gets via the Consumer is provided by the Provider in the FormSection, not by the Provider in the Form component. The Consumer will consume the value provided by the nearest Provider.

<FormConsumer>
  {(formContextValue) => {
    return (
      <FormProvider
        value={{
          . formContextValue.fieldPrefix:` ${formContextValue.fieldPrefix| | "" ${}name}.`,}} >
        {children}
      </FormProvider>
    );
  }}
</FormConsumer>
Copy the code

test

Unit Test

Mainly used for tool class methods.

Integration Test

Mainly used in Field, FieldArray and other components. Because they do not exist independently of forms, you cannot unit test them.

Note: There was no way to modify a property on instance because React set all the nodes on the props to readOnly (via object.defineProperty). But this can be bypassed by setting props overall.

instance.props = { ... instance.props,subscribeFormAction: mockSubscribeFormAction,
  dispatch: mockDispatch,
};
Copy the code

Auto Fill Form Util

If you have too many forms in your project, it can be a burden for QA testing. At this time, we hope to have an automatic form filling tool to help us improve the efficiency of the test.

When writing this tool, we need to simulate Input events.

input.value = 'v';
const event = new Event('input', {bubbles: true});
input.dispatchEvent(event);
Copy the code

The expectation is that the code above will simulate the DOM input event and then trigger the React onChange event. However, the React onChange event was not triggered. Therefore, no value can be set to the input element.

Because ReactDOM emulates onChange events with logic: The ReactDOM generates onChange events only when the value of the input changes.

React 16+ overwrites the input value setter. For details, see inputValueTracking in ReactDOM. So we just need to get the original value setter, the call call.

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, "v");

const event = new Event("input", { bubbles: true});
input.dispatchEvent(event);

Copy the code

Debug

Print the Log

In the Dev environment, you can Debug using logs. Currently, logs are automatically printed in the Dev environment and not in other environments. Log information includes prevState, Action, and nextState.

Note: Since prevState, Action, nextState are all Object, do not forget to call cloneDeep at print time. Otherwise, there is no guarantee that the last printed value is correct, which means that the final result may not be the value at the time of printing.

The last

React Rx Form This article only describes the ideas and core technologies of React Rx Form. You can also implement a version of React Rx Form by following these ideas. Of course, you can also refer to my implementation, welcome to make suggestions and issues. Github address: github.com/reeli/react…