Я думаю, что каждый хоть раз в своей карьере работает над проектами, где у вас много форм. С такой задачей я сталкиваюсь каждый раз, когда разрабатываю панель администрирования или платформу с множеством поисковых форм, и эта рутина меня очень угнетает.
После нескольких проектов я разработал простое решение, которым хочу поделиться с вами. Итак, приступим!
Идея этого решения заключается в создании компонента формы, который контролирует состояние данных формы и всех компонентов формы.
Первый шаг
Начнем с настройки проекта. Например, я буду использовать команду create-react-app
. После того, как приложение настроено, мы можем приступить к разработке.
Сначала мы напишем несколько компонентов управления формой: ввод и выбор. В этой концепции каждый компонент управления формой должен реализовывать определенный интерфейс:
- Каждый элемент управления может иметь метод
onChange
- Каждый элемент управления должен использовать свойство
name
для определения ключа в данных формы. - Каждый элемент управления может использовать свойство
value
для значения в данных формы. - Каждый элемент управления может иметь свойство
label
и свойствоclassName
.
Мы будем управлять этим интерфейсом с помощью базового типа опоры FormControlBasePropTypes
и поместим определение в файл base-prop-types.js
.
import PropTypes from 'prop-types'; export const FormControlBasePropTypes = { className: PropTypes.string, label: PropTypes.string, name: PropTypes.string.isRequired, onChange: PropTypes.func, value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]), };
onChange
и value
являются необязательными, потому что мы не будем определять эти свойства непосредственно для компонента, но поговорим об этом позже.
Изготовление компонентов
Для компонентов управления формой мы определим базовые стили и структуру HTML, эта структура должна зависеть от дизайна и запроса проекта, поэтому предположим, что у нас есть элементы управления формой с отдельной меткой под вводом. Тогда компонент Input
будет содержать следующий код:
import React from 'react'; import classnames from 'classnames'; import { FormControlBasePropTypes } from '../base-prop-types'; import './Input.css'; const Input = ({ className, label, name, value, onChange, }) => ( <label className={classnames('form-control', className)}> {label && ( <span className="form-control__label"> {label} </span> )} <input className="input form-control__input" name={name} value={value} onChange={onChange} /> </label> ); Input.propTypes = { ...FormControlBasePropTypes, }; Input.defaultProps = { className: '', label: '', value: '', }; export default Input;
Здесь я использую classnames
для простого и гибкого управления именами классов.
Для компонента Select
мы также должны определить дополнительную опору options
для определения параметров выбора. Компонент Select
будет содержать следующий код:
import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { FormControlBasePropTypes } from '../base-prop-types'; import './Select.css'; const Select = ({ className, label, name, value, options, onChange, }) => ( <label className={classnames('form-control', className)}> {label && ( <span className="form-control__label"> {label} </span> )} <select className="select form-control__input" name={name} value={value} onChange={onChange} > <option value="">---</option> {options.map(option => ( <option key={option} value={option}> {option} </option> ))} </select> </label> ); Select.propTypes = { ...FormControlBasePropTypes, options: PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]) ).isRequired, }; Select.defaultProps = { className: '', label: '', value: '', }; export default Select;
Наконец, мы можем начать с самого Form
компонента. Ядро компонента будет выглядеть следующим образом:
import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import './Form.css'; const Form = ({ className, children, }) => ( <form className={classnames('form', className)}> {children} <button className="button form__submit" type="submit" > Submit </button> </form> ); Form.propTypes = { children: PropTypes.oneOfType([ PropTypes.arrayOf( PropTypes.node, ), PropTypes.node, ]).isRequired, className: PropTypes.string, }; Form.defaultProps = { className: '', }; export default Form;
Отображение формы внутри любого родительского компонента будет выглядеть следующим образом. Обратите внимание, что на этом шаге должно быть определено name
prop.
<Form className="app__form"> <Input label="Some input" name="input" /> <Select label="Some select" name="select" options={['one', 'two', 'three']} /> </Form>
Вы можете подумать: «Ну и что? Здесь ничего интересного ». но, пожалуйста, проявите терпение. Скоро начнется «волшебство».
Соединив все вместе, мы получаем компоненты Select
и Input
внутри компонента Form
, но результат неудовлетворительный, консоль разработчика полна ошибок и все вообще не работает.
Итак, давайте сделаем трюк!
Магия
В React вы можете перебирать дочерние элементы и применять некоторые свойства к компонентам с помощью функции React.cloneElement
. И именно этим мы и займемся дальше.
Итак, вместо рендеринга children
непосредственно в Form
, мы переберем в цикле всех дочерних элементов и применим некоторые дополнительные свойства, которые мы определили в FormControlBasePropTypes
. Также мы применим опору className
, чтобы все элементы управления выглядели одинаково в форме.
Form
будет хранить значение каждого элемента управления в хранилище, поэтому мы сначала определяем хранилище.
const [formValue, setFormValue] = useState({});
Затем мы определяем обработчик обновления formValue
. Мы ожидаем, что объект Event
будет активирован нашим элементом управления после внутренних событий onChange
, поэтому мы можем извлечь name
и value
.
const onFormValueUpdate = ({ target: { name, value } }) => { setFormValue({ ...formValue, [name]: value, }); };
Код дочернего рендерера превращается в цикл map
со следующим кодом:
React.Children .toArray(children) .map((child) => React.cloneElement(child, { value: formValue[child.props.name] || '', className: classnames(child.props.className, 'form__control'), onChange: onFormValueUpdate, }))
Здесь мы добавили value
компонента, className
и onChange
обработчик. Теперь наши компоненты выглядят лучше, и данные формы заполняются. Но у нас по-прежнему нет доступа к данным формы.
Давайте завершим нашу форму. Мы должны добавить опору onSubmit
к нашему Form
компоненту и добавить обработчик к тегу формы.
const Form = ({ className, children, onSubmit, }) => { ... const onSubmitHandler = (e) => { e.preventDefault(); onSubmit(formValue); }; return ( <form onSubmit={onSubmitHandler} className={classnames('form', className)} > ...
Теперь у нас есть полностью рабочая и расширяемая форма с сохранением состояния.
И даже console.log
данных нашей формы работают!
{
input: “some text”,
select: “two”
}
Заключение
Этот пример не идеален и должен быть дополнен тестами, дополнительными проверками и компонентами. Например, вы можете добавить кнопку сброса формы, подсветку ошибок и так далее.
Я надеюсь, что сама идея поможет вам в вашей рутине построения формы.
Вы можете найти полный код на Github по ссылке.
Не стесняйтесь комментировать, я открыт для любых предложений и критики.
Спасибо за внимание!
Примечание из JavaScript In Plain English
Мы запустили три новых издания! Проявите любовь к нашим новым публикациям, подписавшись на них: AI на простом английском, UX на простом английском, Python на простом английском - спасибо и продолжайте учиться!
Мы также всегда заинтересованы в продвижении качественного контента. Если у вас есть статья, которую вы хотели бы отправить в какую-либо из наших публикаций, отправьте нам электронное письмо по адресу [email protected] с вашим именем пользователя Medium, и мы добавим вас в качестве автора. Также сообщите нам, к каким публикациям вы хотите быть добавлены.