Я думаю, что каждый хоть раз в своей карьере работает над проектами, где у вас много форм. С такой задачей я сталкиваюсь каждый раз, когда разрабатываю панель администрирования или платформу с множеством поисковых форм, и эта рутина меня очень угнетает.

После нескольких проектов я разработал простое решение, которым хочу поделиться с вами. Итак, приступим!

Идея этого решения заключается в создании компонента формы, который контролирует состояние данных формы и всех компонентов формы.

Первый шаг

Начнем с настройки проекта. Например, я буду использовать команду 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, и мы добавим вас в качестве автора. Также сообщите нам, к каким публикациям вы хотите быть добавлены.