Preface – Problem definition

Requirement definition: it can generate corresponding form components and return structured form data by defining JSON data templates in accordance with certain specifications. Contains basic extensible form atomic components and layout components, while the nesting hierarchy can be controlled freely to achieve some degree of optimal rendering efficiency.

Problem decomposition (TLDR) :

  • Define the form component template: form generator model
  • Design of infinitely nested components: Data structure design for trees
  • Sibling linkage issues: control/dependencies are transferred to the parent
  • Component state management issues: controlled and uncontrolled component selection
  • Optimization data update problem: non-variable data optimization + data update detection
  • Node data update problem: In useReducer mode

Define the form component template

JSON form generation: describes the appearance style, control type, and control extension properties of components in JSON format, generates interface components by translating the DESCRIPTION information in JSON format, and provides certain interactive functions (for example, the linkage between components).

For example, there is a basic form structure like this:

{
  "label": "Username"."key": "userName"."value": null."default": "Stephen"."type": "Input" // The definition is based on the Antd control and other types of custom components such as CheckBox, Select, Group, Tabs, and CustomA
}
Copy the code

Type General component Type (this article uses Ant Design component as an example 1). General components such as CheckBox, Select, Input, etc. We also need layout components such as Collapse Collapse panel, Tabs, etc.

Type Indicates the user-defined component Type. It can be a component based on the service system, an interactive component, or an extension of basic components, such as Font color and Font weight.

Design of infinite nested components

After having the basic component form template, how can we more freely assemble the desired structure through the definition of the form, such as building blocks, just need enough basic elements and basic element types, can be infinite expansion.

Infinite nesting: the realization of infinite nesting many functions, the more common is the tree list control, here also applies to the tree structure as the basis of infinite nesting.

Extended form template:

{
  "label": "Username"."key": "userName"."value": null."default": "Stephen"."type": "Input"./ /... Other attributes
  "children": [] // Children is an array of form structures
}
Copy the code

There is nothing new or difficult about the design of the tree structure, but the slight complexity lies in the data callback update and rendering optimization of the tree components, which will be covered in the following sections.

Third, the linkage problem of sibling nodes

Sibling node linkage: From the perspective of service requirements, there is A certain “constraint relationship” between components. The change of component A affects the appearance or value range of component B. For tree components, such constraints often exist in the “same level”, and linkage between cross-parent components rarely occurs. If there is, it can be considered whether it is a problem in function division or extend cross-parent component dependency support

Since this is a pre-sibling relationship problem, the best solution is to “schedule promotion” to the parent, which “coordinates” the relationship between the children. Another possible scenario is for the child component to listen for the value of the target child component and influence its own change. This may not be good or bad for simple requirements, but there are a few other factors worth looking at.

classification Parent component coordination Subcomponent monitoring
dependency From top to bottom From the bottom up
DAG detection
The component patterns Controlled component mode Uncontrolled component mode

In this paper, the parent component is mainly adopted to “coordinate” the child component. The main reason is that the control can be more flexible and the advantages of uncontrolled components can be fully utilized. The choice between controlled and uncontrolled components will be discussed later.

Before we extend the form template, we need to identify a few elements that implement the functionality:

  • The Key value of the dependent component
  • Change function/evaluation function fn
  • The value or appearance that the child component itself can change

First of all, we can define dePS to specify the Key value of the dependent component. With the Key value, we can obtain the current value of the dependent component. If more advanced transformation is needed, we can use the custom function F to transform, and finally return the specific attribute value to change itself. As A common example, component A’s “Show Label” is A checkbox component. When Show Label is enabled, component B’s Input can be in edit mode; otherwise, it cannot be edited.

NOTE: The extension of the file to Javascript is convenient for display. In terms of usage, the simple JSON data format does not support the saving of functions, so manual conversion is required

The extended template is as follows:

[
  / / A component
  {
    label: "Show Label".type: "Checkbox".key: "enableShowLable".default: false
  },
  / / B component
  {
    label: "Label Content".type: "Input".key: "enableShowLable".disabled: true.watcher: {
      deps: ["enableShowLabel"].action: props= > {
        return {
          disabled:! props.enableShowLable }; }}}];Copy the code

Next comes the design of the parent component, which can be detailed in the JS Doc:

/** * gets the value that the child component depends on and passes it to the child component *@param { deps: [], action: Function } watcher- The Watcher property * for the child component@param { Array } children- All current sibling nodes *@returns {any}* /
const getDependencyValue = useCallback((watcher, children) = > {
  if(watcher? .deps) {// Note: only support depend on one property for now.
    constdependencyKey = watcher? .deps?.[0];
    returnchildren? .find(r= >r.key === dependencyKey)? .value; }} []);/** * Gets the property content of the child component change *@param { Function } action- The Watcher property * for the child component@param { string } key- You need to change the Key value of an attribute, for example, Disabled *@param { any } value- Current dependent node *@returns {any} The changed value */
const invokeDependencyWatcher = (action, key, value) = > {
  if(! action) {return {};
  }
  const item = action({
    [key]: value
  });
  return item;
};
Copy the code

NOTE: Change the component itself through the value and operation of the parent component. What is not implemented here is the detection of DAG dependence and the “safe range” for the child component to update its own attribute values.

Through the above, the basic structure of a component custom form component generator is basically completed. Next, the design and optimization of some custom forms are discussed.

4. Component state management

As for the topic of “state management”, I don’t want to expand too much, what I need to discuss here is whether the design of atomic components is self-maintained internally or completely dependent on external input, what are the advantages and disadvantages, and how to choose between them.

4.1 What is a controlled component

In HTML, form elements such as input, Textarea, and SELECT typically maintain their own state and are updated based on user input. In React, mutable state is usually stored in the component’s state property, and 2 can only be updated by using setState(). In most cases, we recommend using controlled components to process form data. In a controlled component, form data is managed by the React component 3.

In short, we have complete say and freedom in how components are used. In the React component case, the controlled component means manually binding the value and updating the value after onChange, for example:

handleChange(event) {
  this.setState({value: event.target.value});
}

render () {
  return (<input type="text" value={this.state.value} onChange={this.handleChange} />);
}
Copy the code

4.2 What are uncontrolled components

Because uncontrolled components store real data in DOM nodes, it is sometimes easier to integrate React and non-React code together when using uncontrolled components. If you don’t care about code aesthetics and want to write code quickly, using uncontrolled components can often reduce your code load. Otherwise, you should use controlled components. 3

In contrast to a controlled component, an uncontrolled component may care only about the defaultValue of defaultValue and the final output change value, not about how it is encapsulated internally, and perhaps not about when a data change is triggered, but about getting the value when it needs to know the value of its own component state, for example:

<DatePicker
  defaultValue={this.state.time}
  ref={input= > (this.input = input)}
/>
Copy the code

4.3 Differences and Connections

From the perspective of the user of the component, there are the following differences:

feature uncontrolled controlled
Get control values
Validate control values as they change
Formatting input
Dynamic change value
External trigger refresh rendering is supported
Whether internal component refresh is controllable

From the component designer’s point of view there are the following differences:

feature uncontrolled controlled
Code is required to maintain state
Delayed storage change status

To summarize, here shows the definition of what is controlled components and uncontrolled components and the difference, in view of the “infinite” cascade form design, “supports external trigger refresh rendering”, “dynamic change value”, “internal component refresh is controlled” these factors will restrict the selection of the component patterns, especially for the components in the component tree china-africa leaf node, Controlled components are a better choice. For the components of the leaf nodes, of course, if the business logic is complex and there are “significant” for temporary status data (here refers to the complex business logic will produce a large number of temporary state, is designed according to personal experience, there is no exact measurable guideline values, please make your own weighing trade-offs), encapsulated a preference for some private business custom components more appropriate.

5. Optimize data update

This raises the question of how to do a “local” refresh, rather than a global refresh, in a component tree where nodes change in one branch but not in the other, without incurring additional browser performance costs.

The essence of this problem is how to determine whether a component “changes” when it listens for its own dependency (for example, the value entered by the component). For simple types, such as string and number types, we can rely on whether the value itself has changed, but for object types the situation is a little more complicated:

  • The values contained in the object are of type Primitive (stirng, Number, Bigint, Boolean, undipay, symbol…) 5
  • The value contained in the Object is of type Object, which is very hierarchical. It’s not particularly easy to exhaustive the Primitive type of an Object

The Immutable data structure is used to compare the value of an Object with the Immutable Tools. Here take ImmutableJS example 6, but also can choose ReduxJS select Immutable data structure library ImmerJS7, the principle of the two implementation is slightly different.

import { Map, is } from "immutable";
const map1 = Map({ a: 1.b: 1.c: 1 });
const map2 = Map({ a: 1.b: 1.c: 1 });

// Comparison mode
map1 === map2; //false
Object.is(map1, map2); // false
is(map1, map2); // true
Copy the code

Now that we have tools and methods for distinguishing dependency changes, we need to collect the React Memo to determine whether to update the component itself.

// ... improt related resouce

// This function is equivalent to 'shouldComponentUpdate' for the React component lifecycle
const componentShouldUpdateFn = (immutablePrev, immutableNext) = > {
  return immutablePrev === immutableNext;
};

const MyComponent = memo(
  ({ nameObj: { firstName: string, lastName: string, middleName: string } }) = > {
    return <div>{Hello`${nameObj.lastName}`}</div>;
  },
  componentShouldUpdateFn
);
Copy the code

Here, we have briefly implemented how to use Immutable + React Memo to implement component local refresh. How to update the data is not written here, you can refer to the ImmutableJS or ImmerJS API yourself.

6. Update of node data

The components of the Infiniband update themselves and need to update the data set to share state. The core problem is how to locate components on nodes in the tree.

A feasible solution is that when a component is created, you can obtain the unique index KeyChains of the sub-component according to the Key hierarchy between the parent and child components, such as [‘group1’, ‘label’, ‘showLable’]. The corresponding tree chain is as follows:

 - group1
  - label
    - showLabel
    - labelContent
 - group2
  ....
Copy the code

When updating a child component Value, you simply pass in the KeyChains and the changed Value.

Another commonly used scheme is to flatline the tree structure into a layer file structure according to KeyChains, and quickly search and update by converting KeyChains to string keys, such as’ group1.label.showlabel ‘.

conclusion

This article hopes to draw more suggestions and thoughts. There are also a lot of mature products and design ideas in the design industry of form generation components. This paper summarizes some problems and schemes encountered in some projects, hoping to conduct more thinking and exploration for specific problems.

Currently, there are still some deficiencies, such as atomic component layout Settings, custom atomic component dynamic registration, etc., which will be updated in the future.

Refer to the reference


  1. Ant. The design/components /…↩
  2. Zh-hans.reactjs.org/docs/forms….↩
  3. Zh-hans.reactjs.org/docs/uncont…↩
  4. Goshakkk. Name/controlled -…↩
  5. Developer.mozilla.org/en-US/docs/…↩
  6. Juejin. Cn/post / 684490…↩
  7. Segmentfault.com/a/119000001…↩