1. Introduction
I haven’t updated my blog for a long time, so I won’t talk about it, but it’s not a big problem. Today I will talk about it in combination with an example of a high-level component of React written in the project. Combined with the last article, I will deepen my impression
2. The Ant Design Form component
National component library Ant-Design Form library we must have used, more powerful, based on RC-Form packaging, more complete functions
Recently, I encountered a requirement in the project. When the fields of a common form were not filled out, the submit button was disabled. It sounded simple, but antD was used
import { Form, Icon, Input, Button } from 'antd';
const FormItem = Form.Item;
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field= > fieldsError[field]);
}
@Form.create();
class Page extends React.Component<{},{}> {
componentDidMount() {
this.props.form.validateFields();
}
handleSubmit = (e: React.FormEvent<HTMLButtonElement>) = > {
e.preventDefault();
this.props.form.validateFields((err:any, values:any) = > {
if (!err) {
...
}
});
}
render() {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
const userNameError = isFieldTouched('userName') && getFieldError('userName');
const passwordError = isFieldTouched('password') && getFieldError('password');
return( <Form layout="inline" onSubmit={this.handleSubmit}> <FormItem validateStatus={userNameError ? 'error' : ''} help={userNameError || ''} > {getFieldDecorator('userName', { rules: [{ required: true, message: 'Please input your username!' }], })( <Input prefix={<Icon type="user" style={{ color: Placeholder ="Username" />)} </FormItem> <FormItem validateStatus={passwordError? 'error' : ''} help={passwordError || ''} > {getFieldDecorator('password', { rules: [{ required: true, message: 'Please input your Password!' }], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)'}} />} type="password"; FormItem> <FormItem> <Button type="primary" HtmlType ="submit" disabled={hasErrors(getFieldsError())} > </Button> </FormItem> </Form>); }}Copy the code
3. So here’s the problem
The above code looks fine at first glance, but binds each field to a validateStatus to see if the current field has been touched and is correct, and triggers a validation when the component renders, thus enabling the disabled button. But the fatal is just to achieve a disabled effect, write so much code, the actual scenario is to have more than 10 such requirements of the form, there is no way not to write so much template code? So I came up with higher-order components
4. Get to work
Since form.create () adds a Form attribute to this.props to use the API it provides, we looked at the effects we expected
// Use effect
@autoBindForm // Components to implement
export default class FormPage extends React.PureComponent {}Copy the code
To achieve the following effect
- 1.
componentDidMount
Triggers a field validation when - 2. Error messages will appear and you need to kill them
- 3. Then, iterate over all fields of the current component to check whether there is any error
- 4. Provide one
this.props.hasError
Similar fields to the current component. Control buttondisabled
state - 5. Support non-mandatory fields, (igonre)
- 6. Support edit mode (default value)
5. Implement autoBindForm
import * as React from 'react'
import { Form } from 'antd'
const getDisplayName = (component: React.ComponentClass) = > {
return component.displayName || component.name || 'Component'
}
export default (WrappedComponent: React.ComponentClass<any>) => {
class AutoBindForm extends WrappedComponent {
static displayName = `HOC(${getDisplayName(WrappedComponent)}) `
autoBindFormHelp: React.Component<{}, {}> = null
getFormRef = (formRef: React.Component) = > {
this.autoBindFormHelp = formRef
}
render() {
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
/>
)
}
return Form.create()(AutoBindForm)
}
Copy the code
Create the components we need to wrap so that we don’t have to create each page once
Then we get a reference to the form through the wrappedComponentRef provided by ANTD
According to the ANTD documentation, we need the following API to achieve the desired effect
validateFields
Verify the fieldgetFieldsValue
Gets the value of the fieldsetFields
Sets the value of the fieldgetFieldsError
Gets the error information for the fieldisFieldTouched
Gets whether the field has been touched
class AutoBindForm extends WrappedComponent
Copy the code
Inheriting the components we need to wrap (known as reverse inheritance), we can validate the fields at initialization time
componentDidMount(){
const {
form: {
validateFields,
getFieldsValue,
setFields,
},
} = this.props
validateFields()
}
}
Copy the code
Since the user entered the page without input, you need to manually clear the error message
componentDidMount() {
const {
form: {
validateFields,
getFieldsValue,
setFields,
},
} = this.props
validateFields()
Object.keys(getFieldsValue())
.forEach((field) = > {
setFields({
[field]: {
errors: null.status: null,},})})}}Copy the code
GetFieldsValue () allows you to dynamically retrieve all the fields in the current form, and then use setFields to iterate and set the error status of all fields to null, so that we achieve the effect of 1,2.
6. Realize real-time error judgment hasError
Since the child component needs a state to know if the current form has an error, we define a value of hasError to implement it. Since it’s real-time, it’s not hard to think of getters to implement it.
Those of you familiar with Vue may think of the computational properties implemented by object.DefinedPropty,
Essentially, the form field collection provided by Antd also triggers page rendering via setState. In the current scenario, the same effect can be achieved directly using the GET property supported by ES6
get hasError() {
const {
form: { getFieldsError, isFieldTouched }
} = this.props
let fieldsError = getFieldsError() as any
return Object
.keys(fieldsError)
.some((field) = >! isFieldTouched(field) || fieldsError[field])) }Copy the code
The code is simple. Every time the getter is triggered, we use the some function to see if the current form has been touched or has an error. In the case of creating the form, if it has not been touched, it has not been entered, so there is no need to verify that there is an error
Finally, pass hasError to the child component at Render
render() {
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
{. this.props}
hasError={this.hasError}
/>} // Parent console.log(this.prop.haserror)<Button disabled={this.props.hasError}>submit</Button>
Copy the code
And let’s define type
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
Copy the code
Now, the scenario where you create a form, basically, you can easily do it with this high-level component, but there are some forms that have some non-mandatory fields, and then you get a blank that doesn’t have to be filled but you think it’s wrong, so let’s improve the code
7. Optimize components to support non-mandatory fields
Optional fields are considered a configuration item and the caller tells me which fields are optional. At the time, I wanted to automatically find out which fields of the current component are not requried, but ANTD’s documentation looked like a mod, so I gave up
First modify the function and add a layer of currying
export default (filterFields: string[] = []) =>
(WrappedComponent: React.ComponentClass<any>) = >{}Copy the code
@autoBindForm(['fieldA'.'fieldB']) // Components to implementexport default class FormPage extends React.PureComponent {
}
Copy the code
Modify hasError logic
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
constisEdit = !! defaultFieldsValuelet fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) = >! isFieldTouched(field)).concat(needIgnoreFields)if(! isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) }return Object
.keys(fieldsError)
.some((field) = > {
constisCheckFieldTouched = ! isEdit || isEmpty(getFieldValue(field))returnisCheckFieldTouched ? (! isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) }Copy the code
The logic is pretty crude, you go through the field that you want to filter, see if it’s been touched, and if it has been touched, you don’t add error validation
Similarly, when you initialize it, you filter it,
First, get all fields of the current form through object.keys (getFieldsValue), because I don’t know which fields are requierd at this time, clever me
This function returns the error value of the current form. Non-required fields do not have errors, so you just need to get the current error information and compare the different values with all fields. Use the xOR function of loadsh to do this
const filterFields = xor(fields, Object.keys(err || []))
this.setState({
filterFields,
})
Copy the code
Finally, clear all error messages
Complete code:
componentDidMount() {
const {
form: {
validateFields,
getFieldsValue,
getFieldValue,
setFields,
},
} = this.props
const fields = Object.keys(getFieldsValue())
validateFields((err: object) = > {
const filterFields = xor(fields, Object.keys(err || []))
this.setState({
filterFields,
})
const allFields: { [key: string]: any } = {}
fields
.filter((field) = >! filterFields.includes(field)) .forEach((field) = > {
allFields[field] = {
value: getFieldValue(field),
errors: null.status: null,
}
})
setFields(allFields)
})
}
Copy the code
With this wave of changes, the need to support non-mandatory fields is complete
8. The last wave supports default fields
This is as simple as seeing if the child component has a default value. If it has a setFieldsValue, it is done. The child component and the parent component agree on a defaultFieldsValue
The complete code is as follows
import * as React from 'react'
import { Form } from 'antd'
import { xor, isEmpty, omit } from 'lodash'
const getDisplayName = (component: React.ComponentClass) = > {
return component.displayName || component.name || 'Component'
}
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
interface IAutoBindFormHelpState {
filterFields: string[]
}
/ * * * @ * @ param name AutoBindForm needIgnoreFields string [] need to ignore validation fields * @ param {WrappedComponent. DefaultFieldsValue} Object form initial value */
const autoBindForm = (needIgnoreFields: string[] = [] ) = > (WrappedComponent: React.ComponentClass<any>) => {
class AutoBindForm extends WrappedComponent {
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
constisEdit = !! defaultFieldsValuelet fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) = >! isFieldTouched(field)).concat(needIgnoreFields)if(! isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) }return Object
.keys(fieldsError)
.some((field) = > {
constisCheckFieldTouched = ! isEdit || isEmpty(getFieldValue(field))returnisCheckFieldTouched ? (! isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) }static displayName = `HOC(${getDisplayName(WrappedComponent)}) `
state: IAutoBindFormHelpState = {
filterFields: [],
}
autoBindFormHelp: React.Component<{}, {}> = null
getFormRef = (formRef: React.Component) = > {
this.autoBindFormHelp = formRef
}
render() {
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
{. this.props}
hasError={this.hasError}
/>) } componentDidMount() { const { form: { validateFields, getFieldsValue, getFieldValue, setFields, }, } = this.props const fields = Object.keys(getFieldsValue()) validateFields((err: object) => { const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) const allFields: { [key: string]: any } = {} fields .filter((field) => ! filterFields.includes(field)) .forEach((field) => { allFields[field] = { value: getFieldValue(field), errors: null, status: null, }}) setFields (allFields) / / as a result of inherited WrappedComponent so I can get WrappedComponent props the if (this. Props. DefaultFieldsValue) { this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()(AutoBindForm) } export default autoBindFormCopy the code
This way, if the child component has the props for defaultFieldsValue, those values will be set when the page loads, and no error will be triggered
Use 10.
Import autoBindForm from './autoBindForm' class MyFormPage extends React.PureComponent {... @autobindForm (['filedsA','fieldsB']) class MyFormPage extends React.PureComponent {... Class MyFormPage extends React.pureComponent {// MyFormPage extends react.pureComponent {... // xx.js const defaultFieldsValue = {name: 'xx', age: 'xx', rangePicker: [moment(),moment()] } <MyformPage defaultFieldsValue={defaultFieldsValue} />Copy the code
The important thing to note here is that if the component is wrapped with autoBindForm, it is
<MyformPage defaultFieldsValue={defaultFieldsValue}/>
Copy the code
If you want to get the REF, don’t forget the forwardRef
this.ref = React.createRef()
<MyformPage defaultFieldsValue={defaultFieldsValue} ref={this.ref}/>
Copy the code
Do the same with ‘autobindform.js’
render() {
const { forwardedRef, props } = this.props
return( <WrappedComponent wrappedComponentRef={this.getFormRef} {... props} hasError={this.hasError} ref={forwardedRef} /> ) }returnForm.create()( React.forwardRef((props, ref) => <AutoBindForm {... props} forwardedRef={ref} />), )Copy the code
11. Final code
import * as React from 'react'
import { Form } from 'antd'
import { xor, isEmpty, omit } from 'lodash'
const getDisplayName = (component: React.ComponentClass) = > {
return component.displayName || component.name || 'Component'
}
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
interface IAutoBindFormHelpState {
filterFields: string[]
}
/ * * * @ * @ param name AutoBindForm needIgnoreFields string [] need to ignore validation fields * @ param {WrappedComponent. DefaultFieldsValue} Object form initial value */
const autoBindForm = (needIgnoreFields: string[] = []) = > (WrappedComponent: React.ComponentClass<any>) => {
class AutoBindForm extends WrappedComponent {
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
constisEdit = !! defaultFieldsValuelet fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) = >! isFieldTouched(field)).concat(needIgnoreFields)if(! isEmpty(needOmitFields)) { fieldsError = omit(fieldsError, needOmitFields) }return Object
.keys(fieldsError)
.some((field) = > {
constisCheckFieldTouched = ! isEdit || isEmpty(getFieldValue(field))returnisCheckFieldTouched ? (! isFieldTouched(field) || fieldsError[field]) : fieldsError[field] }) }static displayName = `HOC(${getDisplayName(WrappedComponent)}) `
state: IAutoBindFormHelpState = {
filterFields: [],
}
autoBindFormHelp: React.Component<{}, {}> = null
getFormRef = (formRef: React.Component) = > {
this.autoBindFormHelp = formRef
}
render() {
const { forwardedRef, props } = this.props
return( <WrappedComponent wrappedComponentRef={this.getFormRef} {... props} hasError={this.hasError} ref={forwardedRef} /> ) } componentDidMount() { const { form: { validateFields, getFieldsValue, getFieldValue, setFields, }, } = this.props const fields = Object.keys(getFieldsValue()) validateFields((err: object) => { const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) const allFields: { [key: string]: any } = {} fields .filter((field) => ! filterFields.includes(field)) .forEach((field) => { allFields[field] = { value: getFieldValue(field), errors: null, status: null, }}) setFields (allFields) / / attributes Initialize the default value if (this. Props. DefaultFieldsValue) { this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()( React.forwardRef((props, ref) => <AutoBindForm {... props} forwardedRef={ref} />), ) } export default autoBindFormCopy the code
12. Conclusion
Form. Create (Form. Create, Form. Create, Form. Create, Form