1. Введение
Я давно не обновлял свой блог, поэтому я не буду говорить об этом, но это не большая проблема.Сегодня я расскажу об этом на примере высокоуровневого компонента React, написанного в проекте, и объедините ее с предыдущей статьей, чтобы усилить мое впечатление.
2. Компонент форм Ant Design
Национальная библиотека компонентовAnt-Design
изForm
Библиотека наверняка использовалась всеми, она относительно мощная, основана наrc-form
Пакет, полная функция
Недавно в проекте столкнулся с требованием, обычная форма, когда поля формы не заполнены, кнопка отправкиdisabled
Статус, звучит просто, потому что использованиеantd
Я пролистал документацию, скопировал код и обнаружил, что мне нужно много кода
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: 'rgba(0,0,0,.25)' }} />} 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" placeholder="Password" />
)}
</FormItem>
<FormItem>
<Button
type="primary"
htmlType="submit"
disabled={hasErrors(getFieldsError())}
>
登录
</Button>
</FormItem>
</Form>
);
}
}
3. Вот и проблема
На первый взгляд в приведенном выше коде нет ничего плохого, привяжите его к каждому полюvalidateStatus
Посмотрите, было ли затронуто текущее поле и нет ничего плохого, и инициируйте проверку при отображении компонента, таким образом, чтобы достичьdisabled
Цель кнопки, но, черт возьми, просто добитьсяdisabled
После написания такого большого количества кода фактический сценарий заключается в том, что существует более 10 форм с этим требованием. Есть ли способ избежать написания такого большого количества шаблонного кода? Вот я и подумал об этом.高阶组件
4. Приступайте к работе
из-заForm.create()
отдам позжеthis.props
Добавить кform
свойства, чтобы использовать API, который он предоставляет, после наблюдения мы ожидаем следующих эффектов
// 使用效果
@autoBindForm //需要实现的组件
export default class FormPage extends React.PureComponent {
}
Для достижения следующего эффекта
- 1.
componentDidMount
При запуске проверки поля - 2. В это время появится сообщение об ошибке, и вам нужно избавиться от сообщения об ошибке в это время.
- 3. Затем пройдитесь по всем полям текущего компонента, чтобы определить, есть ли ошибка
- 4. Обеспечьте
this.props.hasError
Текущему компоненту присваиваются аналогичные поля.disabled
условие - 5. Поддерживать необязательные поля (игнорировать)
- 6. Поддержка режима редактирования (со значением по умолчанию)
5. Реализуйте автопривязку формы
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)
}
первыйForm.create
Давайте посмотрим на компоненты, которые нам нужно обернуть, чтобы нам не приходилось делать это на каждой странице.create
однажды
Мы тогдаantd
который предоставилwrappedComponentRef
понятноform
цитаты
согласно сantd
, нам нужно использовать следующий API для достижения желаемого эффекта
-
validateFields
поле проверки -
getFieldsValue
Получить значение поля -
setFields
установить значение поля -
getFieldsError
Получить информацию об ошибке поля -
isFieldTouched
Получить, было ли затронуто поле
class AutoBindForm extends WrappedComponent
Наследуя компоненты, которые нам нужно обернуть (так называемое обратное наследование), мы можем проверять поля во время инициализации
componentDidMount(){
const {
form: {
validateFields,
getFieldsValue,
setFields,
},
} = this.props
validateFields()
}
}
Поскольку пользователь не вводил данные при входе на страницу, необходимо вручную очистить сообщение об ошибке
componentDidMount() {
const {
form: {
validateFields,
getFieldsValue,
setFields,
},
} = this.props
validateFields()
Object.keys(getFieldsValue())
.forEach((field) => {
setFields({
[field]: {
errors: null,
status: null,
},
})
})
}
}
пройти черезgetFieldsValue()
Мы можем динамически получить все поля текущей формы, а затем использоватьsetFields
Пройдите его и установите статус ошибки всех полей наnull
, так что мы достигаем эффекта 1,2,
6. Оценка ошибок в режиме реального времени hasError
Поскольку дочерним компонентам нужно состояние, чтобы знать, есть ли ошибки в текущей форме, мы определяемhasError
Значение , чтобы достичь, так как это в реальном времени, нетрудно придумать использованиеgetter
реализовать,
знакомыйVue
студентов могут подуматьObject.definedPropty
Реализованы вычисляемые свойства,
По сутиAntd
Предоставленная коллекция полей формы также предоставляетсяsetState
, чтобы запустить отрисовку страницы,
В текущей сцене напрямую используйтеes6
поддерживаетсяget
свойства для достижения того же эффекта
код показывает, как показано ниже
get hasError() {
const {
form: { getFieldsError, isFieldTouched }
} = this.props
let fieldsError = getFieldsError() as any
return Object
.keys(fieldsError)
.some((field) => !isFieldTouched(field) || fieldsError[field]))
}
Код очень простой, каждый разgetter
При срабатывании мыsome
функция, чтобы определить, была ли затронута текущая форма или есть ли ошибка.В сценарии создания формы, если она не была затронута, она не должна была быть введена, поэтому нет необходимости проверять, есть ли ошибка
Наконец вrender
когда будетhasError
передается дочернему компоненту
render() {
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
{...this.props}
hasError={this.hasError}
/>
)
}
//父组件
console.log(this.prop.hasError)
<Button disabled={this.props.hasError}>提交</Button>
При этом определяем тип
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
Написанная здесь сцена создания формы в основном может быть легко выполнена с помощью этого компонента высокого порядка, но некоторые формы имеют некоторые необязательные элементы, которые появятся в это время.Необязательные элементы очищаются, но они думают, что есть ошибки, а затем улучшать код
7. Оптимизация компонентов, поддержка необязательных полей
Необязательные поля считаются элементом конфигурации, и звонящий мне скажет, какие поля являются необязательными элементами.В то время я изначально хотел автоматически узнать, какие поля текущего компонента не являются обязательными.requried
Да, ноantd
документация, кажется, была изменена, поэтому я сдался
Сначала измените функцию, чтобы добавить слой каррирования
export default (filterFields: string[] = []) =>
(WrappedComponent: React.ComponentClass<any>) => {
}
@autoBindForm(['fieldA','fieldB']) //需要实现的组件
export default class FormPage extends React.PureComponent {
}
ИсправлятьhasError
логика
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
const isEdit = !!defaultFieldsValue
let fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields)
if(!isEmpty(needOmitFields)) {
fieldsError = omit(fieldsError, needOmitFields)
}
return Object
.keys(fieldsError)
.some((field) => {
const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field))
return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field]
})
}
Логика очень простая и грубая, перебирать поля, которые нужно отфильтровать, чтобы увидеть, было ли прикосновение к нему, если оно было тронуто, никакая проверка ошибок не будет добавлена.
Точно так же отфильтруйте его при инициализации,
сначала черезObject.keys(getFieldsValue)
Получить все поля текущей формы, потому что вы не знаете, какие поля в данный моментrequierd
да, со мной
validateFields
Подтвердите текущую форму, эта функция возвращает значение ошибки текущей формы, необязательные поля не будут иметь ошибок в это время, поэтому вам нужно только получить текущую информацию об ошибке, сравнить разные значения двух со всеми поля, использоватьloadsh
изxor
функция завершена
const filterFields = xor(fields, Object.keys(err || []))
this.setState({
filterFields,
})
Наконец, удалите все сообщения об ошибках
Полный код:
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)
})
}
После такой волны модификаций требование поддержки необязательных полей выполнено.
8. Последняя волна, поддержка полей по умолчанию
На самом деле это очень просто, просто посмотреть есть ли у подкомпонента значение по умолчанию, если даsetFieldsValue
Это делается сразу, дочерний компонент и родительский компонент договариваются об одномdefaultFieldsValue
Полный код выглядит следующим образом
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[]
}
/**
* @name AutoBindForm
* @param needIgnoreFields string[] 需要忽略验证的字段
* @param {WrappedComponent.defaultFieldsValue} object 表单初始值
*/
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
const isEdit = !!defaultFieldsValue
let fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields)
if(!isEmpty(needOmitFields)) {
fieldsError = omit(fieldsError, needOmitFields)
}
return Object
.keys(fieldsError)
.some((field) => {
const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field))
return isCheckFieldTouched ? (!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)
// 由于继承了 WrappedComponent 所以可以拿到 WrappedComponent 的 props
if (this.props.defaultFieldsValue) {
this.props.form.setFieldsValue(this.props.defaultFieldsValue)
}
})
}
}
return Form.create()(AutoBindForm)
}
export default autoBindForm
Таким образом, если есть подсборкаdefaultFieldsValue
Этот реквизит, эти значения будут установлены при загрузке страницы и не вызовут ошибку
10. Использование
import autoBindForm from './autoBindForm'
# 基本使用
@autoBindForm()
class MyFormPage extends React.PureComponent {
...没有灵魂的表单代码
}
# 忽略字段
@autoBindForm(['filedsA','fieldsB'])
class MyFormPage extends React.PureComponent {
...没有灵魂的表单代码
}
# 默认值
// MyFormPage.js
@autoBindForm()
class MyFormPage extends React.PureComponent {
...没有灵魂的表单代码
}
// xx.js
const defaultFieldsValue = {
name: 'xx',
age: 'xx',
rangePicker: [moment(),moment()]
}
<MyformPage defaultFieldsValue={defaultFieldsValue} />
Здесь следует отметить, что если вы используетеautoBindForm
Упакованный компонент
<MyformPage defaultFieldsValue={defaultFieldsValue}/>
В это время хочу получитьref
, не забудьforwardRef
this.ref = React.createRef()
<MyformPage defaultFieldsValue={defaultFieldsValue} ref={this.ref}/>
Аналогичным образом измените autoBindForm.js.
render() {
const { forwardedRef, props } = this.props
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
{...props}
hasError={this.hasError}
ref={forwardedRef}
/>
)
}
return Form.create()(
React.forwardRef((props, ref) => <AutoBindForm {...props} forwardedRef={ref} />),
)
11. Окончательный код
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[]
}
/**
* @name AutoBindForm
* @param needIgnoreFields string[] 需要忽略验证的字段
* @param {WrappedComponent.defaultFieldsValue} object 表单初始值
*/
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
const isEdit = !!defaultFieldsValue
let fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields)
if (!isEmpty(needOmitFields)) {
fieldsError = omit(fieldsError, needOmitFields)
}
return Object
.keys(fieldsError)
.some((field) => {
const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field))
return isCheckFieldTouched ? (!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)
// 属性劫持 初始化默认值
if (this.props.defaultFieldsValue) {
this.props.form.setFieldsValue(this.props.defaultFieldsValue)
}
})
}
}
return Form.create()(
React.forwardRef((props, ref) => <AutoBindForm {...props} forwardedRef={ref} />),
)
}
export default autoBindForm
12. Заключение
такая параForm.create
Переупакованные высокоуровневые компоненты решили некоторые болевые точки и сэкономили много шаблонного кода.Хотя я столкнулся со всеми видами странных проблем во время упаковки, все они были решены, никаких проблем, и это также укрепило мое понимание высокоуровневых компонентов. компонентов заказа, ускользнуло :)