Формы должны быть простыми и декларативными

в 15:27, , рубрики: javascript, React, ReactJS, TypeScript

Формы должны быть простыми и декларативными - 1

Многие вставали перед выбором той или иной библиотеки для работы с формами в ReactJS. Когда я выбирал подходящую мне, разные библиотеки казались идеальными НО: форма на конфигах или колбеки в onSubmit эвенте, или асинхронный submit. Почему формы для реакта не соответствуют принципам реакта, почему они выглядят как что-то особенное? Если эти вопросы приходили вам в голову, или вы любите формы, приглашаю к прочтению статьи.

Давайте представим формы какими они должны быть.

Форма в реакте должна:

  • предоставлять управляемость полей и событий
  • максимально соответствовать html проекции
  • соблюдать декларативность и композицию
  • использовать типичные методы работы с React компонентами
  • иметь предсказуемое поведение

Форма в реакте не должна:

  • задавать модель управления
  • иметь избыточное состояние или требовать дополнительные данные
  • требовать настройку или обязательное использование функций-хелперов

Теперь попробуем описать идеальную форму опираясь на эти правила:

<Form action="/" method="post">
    <Validation>
        <Field type="text" name="firstName">
        <Field type="text" name="lastName">
        <Transform>
            <Field type="number" name="minSalary">
            <Field type="number" name="maxSalary">
        <Transform>
        <Field type="email" name="email">
    </Validation>
    <button type="submit">Send<button>
</Form>

Выглядит практически как обычная html форма, за исключением Field вместо input и неизвестных Validation и Transform. Вы, наверное, уже догадались что тег Validation должен проверять значение полей и создавать сообщения ошибки для них. Тег Transform в свою очередь необходим для вычисления полей minSalary и maxSalary.

Я что-то говорил про React?

Перенесёмся в реалии реакта и опишем ту же форму:

class MySexyForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            model: {}
        };

        this.validator = (model, meta) => {
            let errors = { ...meta.submitErrors };
            if(model.firstName && model.firstName.length > 2) {
                errors = { firstName: ["First name length must be at minimum 2"] };
            }
            if(model.lastName && model.lastName.length > 2) {
                errors = { 
                    ...errors,
                    lastName: ["Last name length must be at minimum 2"] 
                };
            }
            return errors;
        };
        this.transformer = (field, value, model) => {
            switch (field) {
                case "minSalary":
                    if (parseInt(value) > parseInt(model.maxSalary)) {
                        return {
                            maxSalary: value
                        };
                    }
                case "maxSalary":
                    if (parseInt(value) < parseInt(model.minSalary)) {
                        return {
                            minSalary: value
                        };
                    }
            }
            return {};
        };
    }
    onSubmit = (event) => (model) => {
        event.preventDefault();
        console.log("Form submitting:", model);

        this.props.sendSexyForm(model); // абстрактный action после выполнения которого в форму приходят ошибки сабмита в виде submitErrors пропа
    }
    onModelChange = (model) => {
        console.log("Model was updated: ", model);
        this.setState({ model });
    }
    render() {
        return (
            <Form 
                action="/" 
                method="post" 
                onSubmit={this.onSubmit}
                onModelChange={this.onModelChange}
                values={this.state.model}
                initValues={this.props.initValues}
            >
            <Validation validator={this.validator} submitErrors={this.props.submitErrors}>
                <Field type="text" name="firstName">
                <Field type="text" name="lastName">
                <Transform transformer={this.transformer}>
                    <Field type="number" name="minSalary">
                    <Field type="number" name="maxSalary">
                <Transform>
                <Field type="email" name="email">
            </Validation>
            <button type="submit">Send<button>
        </Form>
        );
    }
};

Я не стану подробно рассматривать Field компонент, представим что он рендерит input с переданными в Field пропами и дополнительными value и onChange. А так же сообщения об ошибках для данного поля.

Стоит объяснить появление новых полей initValues, values, onModelChange, onSubmit, validator, transformer.

Начнём с пропов добавленных в Form.

Эвент хендлер onSubmit позволяет перехватить эвент сабмита формы получить доступ к этому эвенту и к текущим значениям полей формы через model.

Эвент хендлер onModelChange позволяет отследить изменения в полях формы.
С помощью values мы можем управлять значениями полей, а initValues позволяет задать начальные значения.

Этот базовый функционал предоставляет большинство библиотек для работы с формами в реакте, ничего необычного, всё так как должно быть.

Рассмотрим тег Validation, у него появились два пропа

  1. validator — функция возвращающая ошибки валидации на основе переданных значений полей формы
  2. submitErrors — дополнительное rest поле передающееся вторым аргументом в функцию валидатор, в нём мы передаём полученные с сервера ошибки после сабмита

К сожалению, я не встречал подобной или похожей реализации валидации, хотя она кажется очевидной: у нас есть функция валидации которая получает данные и возвращает на их основе ошибки, никакой side effect логики, всё так, как должно быть в реакте.

Перейдём к компоненту Transform. Он перехватывает изменения у вложенных полей и вызывает функцию — transformer, принимающую три аргумента:

  • field — имя поля в котором произошло изменение
  • value — новое значение этого поля
  • model — текущее значение полей формы с предыдущим значением изменившегося поля

Она должна вернуть объект вида { [field]: value } который будет использован для обновление других полей формы.

Тоже очевидная реализация вычисляемых полей.

И… что мы имеем в итоге?

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

В компоненте Form отсутствуют лишние пропы отвечающие за дополнительный функционал (трансформация и валидация).

Field получает своё значение, информацию об ошибках и колбек функции по средствам контекстов, что позволяет создавать дополнительные компоненты для работы с формой и делегировать ответственность. Сама форма имеет вид схожий с html проекцией, что упрощает понимание.

react-painlessform

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

А так же посмотреть живой пример из статьи.

Спасибо за внимание

Раз вы дочитали до конца, то вы сильно любите формы или статья была интересна, я буду рад прочитать комментарии и услышать ваше мнение по поводу форм в реакте.

Автор: Wroud

Источник

* - обязательные к заполнению поля