Image credit: unsplash.com/
Author: Dong Jianhua
1. The background
There are many b-end business scenarios of cloud music. Compared with C-end business, b-end business has a longer product life cycle and pays more attention to the sorting of scenes. Most of the time developing b-side business is to copy the previous code, which adds a lot of repetitive and boring work.
In fact, the middle and background system can be divided into several common scenarios: forms, tables and charts. Forms involve complex scenarios such as linkage, verification and layout, which are often the points that developers need to spend energy to solve.
Compared with the traditional Ant Design form development method, we think there are the following problems:
- First, the code can’t be serialized and is more commonly used by non-front-end developers
JSON
Describe the form as simple enough - Form validation is not combined with validation state
onChange
In the case of complex linkage, the code will become difficult to maintain, and it is easy to generate a lot of linked list logic- Forms have many mutually exclusive states to sort through, and we want users to be able to easily switch between them
- For some common and generic scenarios, such as a list of forms, you can also extract a workable solution
So although the traditional form development method is flexible enough, I still think there is room for optimization of forms, and make some trade-offs between flexibility and efficiency.
There are also mature forms solutions, such as Formliy and FormRender. Although the above problems have been solved, it is still not comprehensive enough. We need to have our own style plan.
Therefore, in order to improve the development efficiency of the middle and background, so that the front end can spend time on more meaningful things, we summarized a set of forms for complex scenarios solution.
2. Technical solutions
Schema design is the most important part of the technical scheme. The framework architecture and other works are implemented around this part, so I will follow this idea to introduce to you.
2.1 Schema design
The form scheme is developed based on Ant Design, and the Schema is configured in JSON mode, but not in JSON Schema. In fact, many configuration schemes based on JSON Schema have been considered, but JSON Schema is a bit troublesome to write. So the conversion of JSON Schema is just an added capability.
The simplest form fields are simply configured with key, type, and UI.label, as shown in the following code:
const schema = [
{
"key": "name"."type": "Input"."ui": {
"label": "Name"}}, {"key": "age"."type": "InputNumber"."ui": {
"label": "Age"
},
"props": {
"placeholder": "Please enter your age"}}, {"key": "gender"."type": "Radio"."value": "male"."ui": {
"label": "Gender"
},
"options": [{"name": "Male"."value": "male"
},
{
"name": "Female"."value": "female"}}]];export default function () {
const formRef = useRef(null);
const onSubmit = () = > {
formRef.current.submit().then((data: any) = > {
console.log(data);
});
};
const onReset = () = > {
formRef.current.reset();
};
return (
<>
<XForm
ref={formRef}
schema={schema}
labelCol={{ span: 6 }}
wrapperCol={{ span: 12}} / >
<div>
<Button type="primary" onClick={onSubmit}>submit</Button>
<Button onClick={onReset}>reset</Button>
</div>
</>
);
}
Copy the code
Since the solution is based on the Form component of Ant Design, in order to retain some of the features of Ant Design, we designed UI and props fields corresponding to the props of the form. Item and the props of the component, respectively. Even if later Ant Design forms add some functionality or features, this form solution can be seamlessly supported.
2.1.1 Verification mode
Since forms are implemented based on Ant Design, the validation library async-Validator is also used. This library is mature and powerful, and can verify deep-level data types such as Array and Object, meeting the needs of complex validation. So let’s make adjustments directly based on this library.
In addition to async-validator, status and trigger conditions are added as follows:
- Status: indicates the verification status
- Error (default) : indicates an error
- Warning: warning
- Trigger: indicates the trigger condition
- Submit (default) : Triggered when submitting
- “Change” : trigger judgment when the value changes
- Blur: Trigger judgment when out of focus
The basic usage mode is as follows:
{
"key": "name"."type": "Input"."ui": {
"label": "Name"
},
"rules": [{"required": true."message": "Name required"."trigger": "blur"."status": "error"}}]Copy the code
2.1.2 Linkage Mode
In addition to calibration, linkage is also more commonly used functions, traditional way of linkage by onChange component implementation, when the linkage logic is complicated, see code like search list of trouble, so the design of a reverse way of monitoring, field all the change of maintenance in the field configuration itself, reduce maintenance cost.
The listeners are configured with the watch, condition, and set fields to realize the interworking function.
Watch records the fields to be monitored. When there is any change in the monitored field, the condition judgment will be triggered. Only when the condition judgment is passed, the set setting will be triggered.
[{"key": "name"."type": "Input"
},
{
"key": "gender"."type": "Radio"."value": "male"."options": [{"name": "Male"."value": "male"
},
{
"name": "Female"."value": "female"}]."listeners": [{"watch": [ "name"]."condition": "name.value === 'Marry'"."set": {
"value": "female"}}]}]Copy the code
In the preceding example, when the name is Marry, the gender is changed to female by default.
2.1.3 Form status
We found that some linkage scenarios are for hiding and displaying fields. In order to facilitate the user to switch states, four mutually exclusive form states are sorted into a status field:
- State the status:
- Edit (default) : Edit
- Disabled, disabled
- Preview: preview
- Hidden hidden:
The Preview state is not inherent to the component itself, but there was a lot of demand for it, so we extended it to include preview states for all basic form components. Even custom components display field values by default and provide solutions if you need to do it yourself.
The usage is as follows:
[{"key": "edit"."type": "Input"."status": "edit"."value": "Edit"."ui": {
"label": "Edit"}}, {"key": "disabled"."type": "Input"."status": "disabled"."value": "Disabled"."ui": {
"label": "Disabled"}}, {"key": "preview"."type": "Input"."status": "preview"."value": "Preview"."ui": {
"label": "Preview"}}, {"key": "hidden"."type": "Input"."status": "hidden"."value": "Hidden"."ui": {
"label": "Hidden"}}]Copy the code
The renderings are as follows:
2.1.4 the Options Settings
Many selection components use the Options field to set options, sometimes retrieved through an asynchronous interface. Considering the asynchronous interface, four schemes are designed:
options
为Array
In the case
{
"key": "type"."type": "Select"."options": [{"name": "Vegetables"."value": "vegetables"
},
{
"name": "Fruit"."value": "fruit"}}]Copy the code
options
为string
That is, interface links
{
"key": "type"."type": "Select"."options": "//api.test.com/getList"
}
Copy the code
options
为object
The situation,action
Is the interface link,nameProperty
configurationname
Fields,valueProperty
configurationvalue
Fields,path
To get the option path,watch
Configuring listening Fields
{
"key": "type"."type": "Select"."options": {
"action": "//api.test.com/getList?name=${name.value}"."nameProperty": "label"."valueProperty": "value"."path": "data.list"."watch": [ "name"]}}Copy the code
action
为function
In the case
{
"key": "type"."type": "Select"."options": {
"action": (field, form) = > {
return fetch('//api.test.com/getList')
.then(res= > res.json());
},
"watch": [ "name"]}}Copy the code
2.1.5 List of forms
Form A list is a combination of forms, usually consisting of Table and Card scenarios, which can be added or deleted.
This type of form value is returned as an Array, so we designed an Array component to switch the Table and Card forms according to props. Type. Children configure the subform as follows:
{
"key": "array"."type": "Array"."ui": {
"label": "Form list"
},
"props": {
"type": "Card"
},
"children": [{"key": "name"."type": "Input"."ui": {
"label": "Name"}}, {"key": "age"."type": "InputNumber"."ui": {
"label": "Age"}}, {"key": "gender"."type": "Radio"."ui": {
"label": "Gender"
},
"options": [{"name": "Male"."value": "male"
},
{
"name": "Female"."value": "female"}]}]}Copy the code
The renderings are as follows:
2.2 Framework architecture
Around the Schema design idea, we adopted a distributed management scheme, which separated the core layer and the rendering layer. The field information was maintained in the core layer, and the rendering layer was only responsible for rendering, so as to separate the structure of data and interface code.
The core layer communicates with the rendering layer through Sub/Pub, and the rendering layer adjusts the interface by listening to a series of events defined by the core layer.
This kind of data state change driving interface changes is nothing new, and is widely used in most frameworks, which has the following advantages:
- Data and state sharing between fields in the
- Through the control of events, the rendering times can be optimized reasonably and the performance can be improved
- Can adapt to multiple frameworks, just need to reuse a set of core layer code
The core layer is mainly composed of Form, Field, ListenerManager, Validator and optionManager, as shown in the figure below:
The ListenerManager manages the linkage function. The Validator and OptionManager manage the verification and options options function respectively under the Field
2.2.1 Verification implementation
It is mainly implemented through the Async-Validator class library, but it still can not meet the situation of multi-check states and multi-trigger conditions, so we make some extensions on this basis and encapsulate it into a Validator class.
Validator only has a single Validator. Validate method, passing a trigger argument, and parsing the Rules field when instantiating the Validator. Categorize according to trigger and create corresponding Async-Validator instances.
2.2.2 Linkage implementation
The ListenerManager has the ListenerManager.add and ListenerManager.trigger methods, which are used to resolve and add the listeners Field respectively, and trigger the interworking effect when the listeners Field changes.
During the initialization of the Field, the listeners are parsed through the ListenerManager.add method, classified according to the key value in the Watch, and saved there. When the Field information changes, the linkage will be triggered through ListenerManager.trigger to judge whether the condition condition is satisfied. If so, the set content will be triggered.
2.2.3 Form list implementation
The form list is actually composed of multiple XForm instances, each increment is an XForm instance, so linkage can only be performed on the same row, not across rows.
When the Add button is clicked, an XForm instance is created from the Schema template provided by Children:
2.2.4 Layout implementation
In addition to the three layout options provided by Ant Design’s forms (horizontal, vertical, and inline), a more flexible layout is needed to accommodate more complex situations.
Layout is a real headache, especially when implementing a complex Schema layout in a JSON-like structure can easily result in a deep level of Schema nesting, which we do not want to see.
Count or col.count parameter is set to calculate the number of rows and columns in the grid and then distribute the fields. This method is only suitable for the situation where the number of rows and columns is consistent, but this method is difficult to meet the situation where the number of rows and columns is inconsistent:
Therefore, a UI. Groupname field is redesigned. The same groupname field will be wrapped by a div, and the className of div is groupname. Such a scheme is simple but practical.
3. Detail design
3.1 Ignore specific field values
Some scenarios need to ignore the value of the field where status is hidden, so a ignoreValues field is designed.
- Hidden: Ignores the hidden state
- Preview: Ignores the preview status
- Disabled: Ignores the disabled state
- Null: Ignores the case where the value is null
- Undefined: the value of undefined is ignored
- FalseLike: ignores the value == false
By configuring the ignoreValues field, the values returned after the commit is ignored:
<XForm schema={schema} ignoreValues={['hidden', 'null']} / >
Copy the code
3.2 Field deconstruction and reorganization
Field deconstruction refers to the splitting of the value of a field into multiple fields, and field reorganization refers to the combination of multiple fields into one field. The specific function of this part has not been realized, but there are preliminary ideas.
The following is an example of field deconstruction: split the fields by key, and return values containing startTime and endTime:
{
"key": "[startTime, endTime]"."type": "RangePicker"."ui": {
"label": "Time selection"}}Copy the code
It is found that many scenarios need to combine multiple fields into one field. In most cases, custom components need to be written or data needs to be processed later. In order to simplify this process, the function of field reorganization is designed. To recombine multiple fields into a single field through the Combine component:
{
"key": "time"."type": "Combine"."ui": {
"label": "Time selection"
},
"props": {
"shape": "{startTime, endTime, type}"
},
"children": [{"key": "startTime"."type": "DatePicker"
},
{
"key": "endTime"."type": "DatePicker"
},
{
"key": "type"."type": "Select"."options": [{"name": "Release Date"."value": "publishTime"
},
{
"name": "Launch time"."value": "onlineTime"}]}]}Copy the code
4. At the end
The process of perfecting the form product is also a process of learning from others. We investigated the competitive products in the industry and developed this product in combination with our own business needs. The ideas and implementation of the form scheme are introduced above for your reference. Unfortunately, our product is not open source yet. I believe we will meet you at the appropriate time.
5. Relevant information
- Formily
- FormRender
This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!