This article has participated in the good Article call order activity, click to view:Back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!
This is the 107th unwatered original, want to get more original good articles, please search the public number to pay attention to us ~ this article first in the political cloud front blog: the most familiar stranger RC-form
Rc – who is the form?
We may often use third-party component libraries such as Ant Design, Element UI, and Vant to quickly create page layouts and simple interactions in a project.
But what we might overlook is that some of the components in these great third-party libraries may also depend on other great libraries! As we often use the Form component in Ant Design (I’m talking here about the React version).
These great open source libraries use a great third-party library called RC-Form internally, like getFieldDecorator, getFieldsValue, setFieldsValue, validateFields, and so on. These are the methods that rC-form exposes.
Why use rC-form?
As we all know, the design pattern of React framework is different from Vue. In Vue, the author has helped us realize two-way data binding, data-driven view, and view-driven data change, but in React, we need to manually call setState to realize data-driven view change. Please see the code below.
import React, { Component } from "react";
export default class index extends Component {
state = {
value1: "peter".value2: "123".value3: "23"}; onChange1 =({ target: { value } }) = > {
this.setState({ value1: value });
};
onChange2 = ({ target: { value } }) = > {
this.setState({ value2: value });
};
onChange3 = ({ target: { value } }) = > {
this.setState({ value3: value });
};
submit = async() = > {const { value1, value2, value3 } = this.state;
const obj = {
value1,
value2,
value3,
};
const res = await axios("url", obj)
};
render() {
const { value1, value2, value3 } = this.state;
return (
<div>
<form action="">
<label for="">User name:</label>
<input type="text" value={value1} onChange={this.onChange1} />
<br />
<label for="">Password:</label>
<input type="text" value={value2} onChange={this.onChange2} />
<br />
<label for="">Age:</label>
<input type="text" value={value3} onChange={this.onChange3} />
<br />
<button onClick={this.submit}>submit</button>
</form>
</div>); }}Copy the code
-
Above is a simple function of a form login! In order to achieve real-time update of form data, it is necessary to manually update the state state when the form onChange;
-
From the above code, we can see that this writing function can be implemented, but when we have many forms, do the page need to write a dozen onChange events to implement the page’s data-driven view update? It’s not a good way to think about it;
-
This is where RC-Form comes in. Rc-form creates a centralized warehouse that collects form data to validate, reset, set, retrieve values and other logical operations, so that we can leave the repetitive work to RC-Form to achieve high code reusability!
Brief description of the main apis
The name of the Api | instructions | type |
---|---|---|
getFieldDecorator | For bidirectional binding to forms, | Function(name) |
getFieldsValue | Gets the values corresponding to a set of field names and returns them according to the corresponding structure. Default returns the value of an existing field when calledgetFieldsValue(true) Returns all values when |
(nameList? :NamePath[], filterFunc? : (meta: { touched: boolean, validating: boolean }) => boolean) => any |
getFieldValue | Gets the value of the corresponding field name | (name: NamePath) => any |
setFieldsValue | Sets the values of a set of forms | (values) => void |
setFields | Sets the status of a group of fields | (fields: FieldData[]) => void |
validateFields | Trigger form validation | (nameList? :NamePath[]) => Promise |
isFieldValidating | Check whether a group of fields is being validated | (name: NamePath) => boolean |
getFieldProps | Gets the property of the corresponding field name | (name: NamePath) => any |
Use the rc – form
import { createForm } from ".. /.. /rc-form";
// import ReactClass from './ReactClass'
const RcForm = (props) = > {
const {
form: { getFieldDecorator, validateFields },
} = props;
const handleSubmit = (e) = > {
e && e.stopPropagation();
validateFields((err, value) = > {
if(! err) {console.log(value); }}); };return (
<div style={{ padding: 20.background: "#fff}} ">
<form>
<label>Name:</label>{getFieldDecorator("username", {rules: [{required: true, message: "please enter username!"}], initialValue:'initialValue',})(<input type="text" />)}
<br />
<label>Password:</label>{getFieldDecorator("password", {rules: [{required: true, message: "please enter password!"}, {pattern: /^[a-z0-9_-]{6,18}$/, message:' only numbers!'}],})(<input type="password" style={{ marginTop: "15px}} "/ > )}
<br />
<button onClick={handleSubmit} style={{ marginTop: "15px}} ">submit</button>
</form>
</div>
);
};
export default createForm()(RcForm);
Copy the code
Note: Components processed by the createForm method (the Ant Design Form’s create() method) automatically inject Form objects into the component, and the component itself has the Api.
-
Demo is simply based on RC-Form to achieve form decoration, form validation, data collection and other functions. So how do you implement more targeted form components that can be used in multiple business scenarios?
-
Bypass the excellent open source component library aside, if one day these excellent open source works are no longer open source, then what do we do?
-
In order to avoid this, or if only for our own career planning, to make ourselves to the next level, it is necessary to learn the design concept of a good tripartite library. Even looking at other people’s code styles is necessary. In fact, we need to understand the design ideas of RC-Form; By understanding the essence of these great open source works, we can encapsulate our own code base and other great components like Ant Design forms without using open source libraries.
Since createForm
We all know that when we write business components that use forms, we usually use createForm or form.create () methods to wrap our components, so let’s start with this story.
import createBaseForm from './createBaseForm';
function createForm(options) {
return createBaseForm(options, [mixin]);
}
export default createForm;
Copy the code
As you can see, createForm is just an encapsulation. The actual calling function is createBaseForm. Let’s take a look inside the createBaseForm function.
In the image above, you can see that this function uses the closure feature to return a new function whose argument is your business component object. After createBaseForm is internally processed, it returns a component with the Form object injected into it. That is, this createBaseForm is a high-level component.
Then it is clear that Ant Design’s form.create () method is an alternative to the createBaseForm method in rC-Form! The component wrapped with createBaseForm will inject the Form object, and the getFieldDecorator and fieldsStore instance provided in the Form property are key to automatic data collection.
Brief analysis of internal implementation
Let’s start with the original rendering form logic, wrapped in getFieldDecorator for the form components used in our business scenario. Of course, I’m talking about pre-4.0 versions of Ant Design, so let’s start here.
First of all, this article is just a brief analysis of the whole form data two-way binding process, because this is the core of RC-Form, limited energy to deal with the specific details of later slowly study. So let’s see what the getFieldDecorator method does.
getFieldDecorator(name, fieldOption) {
const props = this.getFieldProps(name, fieldOption);
return fieldElem= > {
// We should put field in record if it is rendered
this.renderFields[name] = true;
const fieldMeta = this.fieldsStore.getFieldMeta(name);
const originalProps = fieldElem.props;
fieldMeta.originalProps = originalProps;
fieldMeta.ref = fieldElem.ref;
constdecoratedFieldElem = React.cloneElement(fieldElem, { ... props, ... this.fieldsStore.getFieldValuePropValue(fieldMeta), });return supportRef(fieldElem) ? (
decoratedFieldElem
) : (
<FieldElemWrapper name={name} form={this}>
{decoratedFieldElem}
</FieldElemWrapper>
);
};
},
Copy the code
I’ve removed some of the irrelevant code here because it makes it clearer. We first call getFieldProps to the incoming form component, and then return a function that takes the form component we passed in with the getFieldDecorator. Call getFieldMeta in fieldsStore to get the configuration data of the form component, which is compatible with the configuration properties of the original component and the processing of the ref component that does not support the ref, and finally return a component that is cloned and mounted with the processed configuration objects!
fieldsStore
Since fieldsStore is in use, let’s talk about the fieldsStore, which contains the main information about the current form and some methods for processing the form data.
class FieldsStore {
constructor(fields) {
this.fields = internalFlattenFields(fields);
this.fieldsMeta = {}; }}Copy the code
FieldMeta can be thought of as a description of a form item, with the name passed in as the index key. It can be nested to store the form data, meaning that the configuration information does not involve values.
- Name Indicates the name of the field
- OriginalProps the originalProps of the component decorated by getFieldDecorator()
- Rules Specifies the verification rules
- Trigger Indicates the default time when data collection is triggered
onChange
- Validate Rule and event
- ValuePropName property of the value of a child node such as checkbox should be set to
checked
- GetValueFromEvent How to get a component value from an event
- If hidden is true, this field is ignored for validation or data collection
The fields are used to record the real-time properties of each form, including:
-
Whether the dirty data has changed but has not been verified
-
Errors Check copy
-
Name Field name
-
Whether the touched data has been updated
-
Value Specifies the value of the value field
-
Validating the state
So again, how does the getFieldProps method implement props building inside?
getFieldProps(name, usersFieldOption = {}) {
// Reassemble props
const fieldOption = {
name,
trigger: DEFAULT_TRIGGER,
valuePropName: 'value'.validate: [],
...usersFieldOption,
};
const {
rules,
trigger,
validateTrigger = trigger,
validate,
} = fieldOption;
const fieldMeta = this.fieldsStore.getFieldMeta(name);
// Initial value processing
if ('initialValue' in fieldOption) {
fieldMeta.initialValue = fieldOption.initialValue;
}
/ / assembly inputProps
constinputProps = { ... this.fieldsStore.getFieldValuePropValue(fieldOption),ref: this.getCacheBind(name, `${name}__ref`.this.saveRef),
};
if (fieldNameProp) {
inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
}
// Collect validation rules
const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach((action) = > {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
});
// onCollect is used to collect component values for components that do not perform validation
if (trigger && validateTriggers.indexOf(trigger) === -1) {
inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
}
return inputProps;
},
Copy the code
If the user does not set the trigger and valuePropName, it will use the default values. Then it will call the getFieldMeta method in the fieldsStore. The fieldsStore instance object is particularly critical in this process, serving as a data center that eliminates the need to manually maintain the values bound to the form. So what does Fieldsstore.getFieldMeta do?
getFieldMeta(name) {
this.fieldsMeta[name] = this.fieldsMeta[name] || {};
return this.fieldsMeta[name];
}
Copy the code
This function gets the data center fieldMeta based on the name attribute passed by the component. If not, it defaults to an empty object and returns the initial value for the first rendering. The important part is the assembly of the inputProps. The first step is to call the getFieldValuePropValue method to get the current props. The next step is to add the ref attribute.
const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach((action) = > {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
});
if (trigger && validateTriggers.indexOf(trigger) === -1) {
inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
}
Copy the code
ValidateRules are all of the form component validation rules, and validateTriggers are the names of events triggered by any validation rules. So let’s take a look at how the nomalizeValidateRules and getValidateTriggers methods collect validation rules.
function normalizeValidateRules(validate, rules, validateTrigger) {
const validateRules = validate.map((item) = > {
constnewItem = { ... item,trigger: item.trigger || [],
};
if (typeof newItem.trigger === 'string') {
newItem.trigger = [newItem.trigger];
}
return newItem;
});
if (rules) {
validateRules.push({
trigger: validateTrigger
? [].concat(validateTrigger)
: [],
rules,
});
}
return validateRules;
}
function getValidateTriggers(validateRules) {
return validateRules
.filter(item= >!!!!! item.rules && item.rules.length) .map(item= > item.trigger)
.reduce((pre, curr) = > pre.concat(curr), []);
}
Copy the code
It will combine validate and rules and return an array. The elements inside are rule objects, and each element has a trigger array that can be empty. And pushing the validateTrigger into the validateRules as a rule triggers. Let’s take a look back at the validateTrigger.
const fieldOption = {
name,
trigger: DEFAULT_TRIGGER,
valuePropName: 'value'.validate: [],
...usersFieldOption,
};
const {
rules,
trigger,
validateTrigger = trigger,
validate,
} = fieldOption;
Copy the code
Here you can see that the configured trigger is used by default if the user has configured the trigger validation method, and the default onChange is used if the user has not set the trigger.
GetValidateTriggers collects all triggers into an array, and then binds all events in the validateTriggers to the same handler, getCacheBind, through a forEach loop.
validateTriggers.forEach((action) = > {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(
name,
action,
this.onCollectValidate
);
});
Copy the code
Now, what does the getCacheBind function that triggers the validation rule binding event action do?
getCacheBind(name, action, fn) {
if (!this.cachedBind[name]) {
this.cachedBind[name] = {};
}
const cache = this.cachedBind[name];
if(! cache[action] || cache[action].oriFn ! == fn ) { cache[action] = {fn: fn.bind(this, name, action),
oriFn: fn,
};
}
return cache[action].fn;
},
Copy the code
Ignoring the cachedBind method for a moment, you can see here that the getCacheBind method basically does a change to the fn that’s passed in the logic that this points to, and the real handler is onCollectValidate, So what does onCollectValidate do?
onCollectValidate(name_, action, ... args) {
const { field, fieldMeta } = this.onCollectCommon(name_, action, args);
constnewField = { ... field,dirty: true};this.fieldsStore.setFieldsAsDirty();
this.validateFieldsInternal([newField], {
action,
options: {firstFields:!!!!! fieldMeta.validateFirst,}, }); },Copy the code
When onCollectValidate is called, the onCollectCommon method is called first. What does this function do?
onCollectCommon(name, action, args) {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
if(fieldMeta[action]) { fieldMeta[action](... args); }else if(fieldMeta.originalProps && fieldMeta.originalProps[action]) { fieldMeta.originalProps[action](... args); }constvalue = fieldMeta.getValueFromEvent ? fieldMeta.getValueFromEvent(... args) : getValueFromEvent(... args);if(onValuesChange && value ! = =this.fieldsStore.getFieldValue(name)) {
const valuesAll = this.fieldsStore.getAllValues();
const valuesAllSet = {};
valuesAll[name] = value;
Object.keys(valuesAll).forEach(key= > set(valuesAllSet, key, valuesAll[key]));
onValuesChange({
[formPropName]: this.getForm(), ... this.props }, set({}, name, value), valuesAllSet); }const field = this.fieldsStore.getField(name);
return ({ name, field: { ...field, value, touched: true }, fieldMeta });
},
Copy the code
OnCollectCommon basically takes the latest value of the wrapped component, wraps it in an object and returns it as a new object called newField.
And fieldsStore. SetFieldsAsDirty is component tag packing check state, shall pass through, then perform validateFieldsInternal, we look at the validateFieldsInternal function.
validateFieldsInternal(fields, { fieldNames, action, options = {} }, callback,) {
const allRules = {};
const allValues = {};
const allFields = {};
const alreadyErrors = {};
fields.forEach(field= > {
const name = field.name;
if(options.force ! = =true && field.dirty === false) {
if (field.errors) {
set(alreadyErrors, name, { errors: field.errors });
}
return;
}
const fieldMeta = this.fieldsStore.getFieldMeta(name);
constnewField = { ... field, }; newField.errors =undefined;
newField.validating = true;
newField.dirty = true;
allRules[name] = this.getRules(fieldMeta, action);
allValues[name] = newField.value;
allFields[name] = newField;
});
this.setFields(allFields);
// in case normalize
Object.keys(allValues).forEach(f= > {
allValues[f] = this.fieldsStore.getFieldValue(f);
});
if (callback && isEmptyObject(allFields)) {
callback(
isEmptyObject(alreadyErrors) ? null : alreadyErrors,
this.fieldsStore.getFieldsValue(fieldNames),
);
return;
}
// console.log(allRules);
const validator = new AsyncValidator(allRules);
if (validateMessages) {
// console.log(validateMessages);
validator.messages(validateMessages);
}
validator.validate(allValues, options, errors= > {
consterrorsGroup = { ... alreadyErrors, };// ...
const expired = [];
const nowAllFields = {};
Object.keys(allRules).forEach(name= > {
const fieldErrors = get(errorsGroup, name);
const nowField = this.fieldsStore.getField(name);
// avoid concurrency problems
if(! eq(nowField.value, allValues[name])) { expired.push({ name, }); }else {
nowField.errors = fieldErrors && fieldErrors.errors;
nowField.value = allValues[name];
nowField.validating = false;
nowField.dirty = false; nowAllFields[name] = nowField; }});this.setFields(nowAllFields);
// ...
}
Copy the code
Because validateFieldsInternal’s main logic is calling AsyncValidator for asynchronous validation and handling special scenarios, we’ll skip the data collection part for now. We see that at the end we call this.setfields (allFields); And the new value is passed in, so take a look at the setFields method.
setFields(maybeNestedFields, callback) {
const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
this.fieldsStore.setFields(fields);
if (onFieldsChange) {
const changedFields = Object.keys(fields)
.reduce((acc, name) = > set(acc, name, this.fieldsStore.getField(name)), {});
onFieldsChange({
[formPropName]: this.getForm(), ... this.props }, changedFields,this.fieldsStore.getNestedAllFields());
}
this.forceUpdate(callback);
},
Copy the code
As you can see, setFields first validates the value passed in like an initialization, then calls the setFields method in the fieldsStore instance to store the value in the fieldsStore, ignoring onFieldsChange for the time being. ForceUpdate is then called to update the view. Here, we briefly describe the process.
Two-way binding of form data
The general process of updating form data is as follows:
Conclusion:
-
The action of the user entering or selecting a form component triggers a getFieldDecorator(HOC) high-level component, which then calls getFieldProps to assemble the component props. This method calls the onCollectValidate method to collect validation rules if validateRules and Triggers the validateTriggers in the form component (that is, the rules object). Then you set the latest value of the form component to the fieldsStore and call this.forceUpdate() to update the UI view!
-
If we do not configure rules such as validateRules and validateTriggers, use the onCollect method to collect the latest data and update it to the fieldsStore. Without verifying the form separately, update the UI view by calling this.forceUpdate() in the setFields method to set the latest value!
Overall design idea
Conclusion:
- In general, rC-Form has its own internal state management. The fieldsStore keeps track of all form items, and the form is bi-directional bound via getFieldDecorator.
- The real difference is whether you use a form rule for validation, not onCollect, otherwise onCollectValidate, but always onCollectCommon;
- The onCollectCommon method internally shows the details of the onCollect value, forceUpdate after updating the component, triggers the Render method, and then goes back to the original getFieldDecorator to retrieve the value in the fieldStore. Returns the modified component.
Think about it if I change the value of the input field and it causes the form to rerender. This leads to rendering performance problems! Then there must be optimization methods, interested can see RC-field-form.
The article is only the overall analysis of the realization of ideas, if there are different opinions, welcome to contact me to communicate!
Recommended reading
Vite features and partial source parsing
How do I use Git at work
Serverless Custom (Container) Runtime
Open source works
- Politics in front of tabloids
Open source address www.zoo.team/openweekly/ (there is a wechat group on the homepage of the official website of the tabloid)
, recruiting
ZooTeam (ZooTeam), a young and creative team, belongs to the product RESEARCH and development department of ZooTeam, based in picturesque Hangzhou. The team now has more than 40 front end partners, the average age of 27 years old, nearly 30% are full stack engineers, no problem youth storm team. The membership consists of “old” soldiers from Alibaba and netease, as well as fresh graduates from Zhejiang University, University of Science and Technology of China, Hangzhou Electric And other universities. In addition to the daily business connection, the team also carried out technical exploration and actual practice in the direction of material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, promoted and implemented a series of internal technical products, and continued to explore the new boundary of the front-end technology system.
If you want to change the things you’ve been doing, you want to start doing things. If you want to change, you’ve been told you need to think more, but you can’t change; If you want to change, you have the power to achieve that result, but you are not needed; If you want to change what you want to accomplish, you need a team to support you, but there is no place for you to bring people; If you want a change of pace, it’s “3 years of experience in 5 years”; If you want to change the original savvy is good, but there is always a layer of fuzzy window paper… If you believe in the power of belief, that ordinary people can achieve extraordinary things, that you can meet a better version of yourself. If you want to get involved in the growth of a front end team with a deep understanding of the business, a sound technology system, technology that creates value, and spillover impact as the business takes off, I think we should talk about it. Any time, waiting for you to write something, to [email protected]