Как стать автором
Обновить

Новый пакет валидаций для React на Mobx @quantumart/mobx-form-validation-kit

Время на прочтение 19 мин
Количество просмотров 4.2K
Добрый день.
Сегодня я хочу рассказать о новом пакете для асинхронных валидаций в проектах использующих в своей основе React, Mobx и написанных на Typescript.
Современная Frontend разработка предполагает большое количество логики при заполнении страниц с документами, анкет и документов на кредит, платежных поручений, страниц регистраций на сайте. Основная логическая нагрузка приходиться на валидационные проверки. Разработчики Angular продумали этот момент и предлагают разработчикам для этих целей использовать встроенных механизм FormControl-ов, что, хоть имеет ряд своих недостатков, но всё же лучшем чем полное отсутствие подобного решения на React. Ситуация усложняется еще тем, что современный тренд React разработки предполагает использовать mobx для организации бизнес-логики.
Столкнувшись с этими проблемами мы решили всех их с помощью написания пакета: @quantumart/mobx-form-validation-kit

Плюсы пакета:
  • Полностью на TypeScript
  • Совместимость с Mobx (версии 4, который поддерживает, всеми любимый, IE10)
  • Рассчитан на работу в React (можно использовать в проектах и без react)
  • Рассчитан на асинхронные валидации
  • Легко встроить в существующий проект.

Инструкция по работы с пакетом под катом.


В начале мы опишем функциональные возможности пакета @quantumart/mobx-form-validation-kit, в конце статьи напишем полностью работающую страницу с примером формы регистрации на сайте.

FormControl


@quantumart/mobx-form-validation-kit позволяет создать прослойку между исходными данными и формой для отображения. Что, в свою очередь, позволяет валидировать их и, при необходимости, изменять данных перед тем как они попадут в исходный объект.

Библиотека @quantumart/mobx-form-validation-kit содержит три основных класса (валидационных компонента) для управления формой:
  • FormGroup – позволяет объединять валидационные компоненты вместе. Класс типизированный, и позволяет переделать в качестве generic параметра интерфейс со списком полей. По умолчанию прописан any, крайне не рекомендуется использовать его без типизации, но возможность есть.
  • FormControl – используется для валидации конкретного поля, наиболее часто используемый класс. Класс типизированный, и в качестве generic параметра принимает тип переменной которой должен хранить. По умолчанию прописан string, т.к. по умолчанию является строковым, как наиболее частный вариант для форм.
  • FormArray – позволяет создавать и управлять массивом валидационных компонентов.

Кроме этого есть базовые абстрактные классы
  • AbstractControl – базовый класс для всех перечисленных валидационных классов, не типизирован.
  • FormAbstractControl — базовый класс для FormGroup и FormArray, не типизирован.
  • FormAbstractGroup – не типизированный базовый класс для FormControl, содержит ссылку на html элемент который отрисовывается.

Лучшей практикой по созданию валидирующей формы будет следующая идея.
На форму создается объект типа один FormGroup и в нем уже перечисляются поля
this.form = new FormGroup<IUserInfo>({
      name: new FormControl(
            this.userInfo.name,
            [],
            v => (this.userInfo.name = v)
      ),
      surname: new FormControl(
            this.userInfo.surname,
            [],
            v => (this.userInfo.surname = v)
      )
      // …
    });

FormGroup поддерживает вложенность, т.е.
this.form = new FormGroup<IUserInfo>({
      name: new FormControl(
            this.userInfo.name,
            [],
            v => (this.userInfo.name = v)
      ),
      surname: new FormControl(
            this.userInfo.surname,
            [],
           v => (this.userInfo.surname = v)
      )
      passport: new FormGroup<IPassport >({
            number: new FormControl(
                  this.userInfo.passport.number,
                  [],
                  v => (this.userInfo.passport.number = v)
              ),
              // …
      })
      // …
    });

Можно добавить FormArray, который в свою очередь может быть передан тип FormControl и или целый FormGroup создавая объекты любой сложности и структуры.
  • FormArray<FormControl>
    FormArray< FormGroup>

    Сам по себе FormControl принимает следующий набор параметров в конструктор
    • value: TEntity – типизированное изначальное значение.
    • validators: ValidatorFunctionFormControlHandler[] – набор валидаторов.
    • callbackValidValue: UpdateValidValueHandler | null – callback функция в которое передается последние валидное значение. Она вызывается каждый раз, когда изменилось значение в FormControl и это значение проходит описанные валидации.
    • activate: (() => boolean) | null — функция позволят включать/отключать валидаций по условию (по умолчанию включено всегда). Например, валидность даты окончания услуги не нужно проверять, если не стоит галочка «Безлимитный». Как следствие, просто вписав сюда функцию которая проверив состояния observable поля отвечающего за чекбокс «Безлимитный», можно автоматически отключить все валидации привязанные к полю на проверку даты, а не прописывать эту логику в каждую из валидаций поля дата.
    • additionalData: TAdditionalData | null — блок с дополнительной информацией позволяет добавить дополнительную информацию к конкретному FormControl и использовать их в дальнейшем, например для визуализации. Это удобно, если есть билдеры для FormControl в которых нужно захаркодить определённую информацию, а не передавать это информацию через сложную связку данных в контролы для визуализации. Хотя точного и неоспоримого сценария применения я не смогу привести, но лучше иметь такую возможность, чем страдать без нее.

    Есть одно ограничение, которое также присутствует и FormControl от Angular, не нужно переиспользовать объекты на разных формах. Т.е. можно создать билдер FormGroup и на каждую страницу создавать собственный объект. Но использовать один объект на кучу страниц — плохая практика.
    Более того FormControl инициализируется одним значением, и если это значение будет изменено, новое значение не попадет в FormControl. Сделано это специально, ибо, как показала практика, почему-то, все упорно пытаются изначально править исходный объект в обход валидаций, а не значение в FormControl. Просто присвоите новое значение полю value FormControl чтобы изменить исходный объект.
    FormGroup принимает следующий набор параметров в конструктор:
    • controls: TControls — объект унаследованный от AbstractControls. По факту просто создаете interface унаследованный от AbstractControls в котором перечисляете поля типа FormGroup, FormControl, FormArray. Можно конечно задать тип any, но тогда потеряется все преимущества TypeSсript
    • validators: ValidatorFunctionFormGroupHandler[] – набор валидаторов для групповых значений. Например, можно создать FormGroup содержащий в себе два значения — минимальную и максимально дату, для контролла выбора периода. Именно в эти валидаторы нужно будет передать функцию/функции проверки диапазона дат. Например, что дата начала не больше дата конца
    • activate: (() => boolean) | null — функция позволят включать/отключать валидаций по условию (по умолчанию включено всегда). Надо понимать, что применение функции валидации к группе отключает проверку на уровне всей группы. Например, у нас есть выпадашка выбора документа удостоверяющего личность. Можно создать несколько FormGroup с разным набором полей для документов: паспорт, водительское удостоверение, паспорт моряка и т.д… В этой функции проверять значения выпадашки, и если выбранное значение не соответствует данной группе то отключаются все валидационные проверки. Точнее сказать – группа будет считаться валидной, в независимости от значений в ней.

    Давайте поговорим о полях FormControl, в том числе они присутствую и FormGroup, и в FormArray.
    • ControlTypes — тип контрола (Control | Group | Array)
    • processing: boolean — в процессе анализа. Т.к. поддерживаются асинхронные валидации, нарпимер те, что требуют запроса на сервер. Текущее состояние проверки можно узнать по данному полю.
      Кроме этого FormGroup и FormArray поддерживают метод wait, который позволяет дождаться окончания проверки. Например при нажатии на кнопку «отправить данные» нужно прописать следующую конструкцию.
      await this.form.wait();
      	if (this.form.invalid) {
      	…

    • disabled: boolean — проверка ошибок отключена (контрол всегда валиден)
    • active: boolean — проверка ошибок включена. Зависит от результата выполнения функции активации. Данное значение очень удобно использовать для скрытия группы полей на форме и не писать дополнительные и дублирующие функции бизнес логики.
    • invalid: boolean; — для FormControl – означает, что поле содержит валидационные ошибки. Для FormGroup и FormArray означает, либо сам групповой контрол содержит ошибки, либо одно из вложенных полей (на любом из уровней вложенности) содержит ошибки валидации. Т.е. для проверки валидности всей формы достаточно выполнить одну проверку invalid или valid верхнего FormGroup.
    • valid: boolean — для FormControl – означает, что поле не содержит валидационные ошибки. Для FormGroup и FormArray означает, либо сам групповой контрол не содержит ошибки, и ни одно из вложенных полей (на любом из уровней вложенности) не содержит ошибки валидации.
    • pristine: boolean — значение в поле, после инициализации дефолтным значением, не изменялось.
    • dirty: boolean — значение в поле, после инициализации дефолтным значением, менялось.
    • untouched: boolean — для FormControl – означает, что поле (например input) не было в фокусе. Для FormGroup и FormArray означает, что ни один из вложенных FormControl-ов не был в фокусе. Значение false в этом поле означает, что фокус был не только был поставлен, но и снят с поля.
    • touched: boolean — Для FormControl – означает, что поле (например input) было в фокусе. Для FormGroup и FormArray означает, что один из вложенных FormControl-ов был в фокусе. Значение true в этом поле означает, что фокус был не только был поставлен, но и снят с поля.
    • focused: boolean — для FormControl – означает, что поле (например input) сейчас в фокусе. Для FormGroup и FormArray означает, что один из вложенных FormControl-ов сейчас в фокусе.
    • errors: ValidationEvent[] — поле содержит ошибки валидации. В отличии от перечисленных полей, данный массив содержит именно ошибки либо FormControl, либо FormGroup, либо FormArray, т.е. ошибки данного контрола, а не все вложенные. Влияет на поле valid / invalid
    • warnings: ValidationEvent[] — поле содержит сообщения «Внимание». В отличии от перечисленных полей, данный массив содержит именно ошибки либо FormControl, либо FormGroup, либо FormArray, т.е. сообщения данного контрола, а не все вложенные. Не влияет на поле valid / invalid
    • informationMessages: ValidationEvent[] — поле содержит сообщения «информационные сообщения». В отличии от перечисленных полей, данный массив содержит именно ошибки либо FormControl, либо FormGroup, либо FormArray, т.е. сообщения данного контрола, а не все вложенные. Не влияет на поле valid / invalid
    • successes: ValidationEvent — поле содержит дополнительные сообщения о валидности. В отличии от перечисленных полей, данный массив содержит именно ошибки либо FormControl, либо FormGroup, либо FormArray, т.е. сообщения данного контрола, а не все вложенные. Не влияет на поле valid / invalid
    • maxEventLevel() – максимальный уровень валидационных сообщении содержащих в поле в текущий момент.
    • Метод вернет одно из значений enum, в следящем приоритете.
      1. ValidationEventTypes.Error;
      2. ValidationEventTypes.Warning;
      3. ValidationEventTypes.Info;
      4. ValidationEventTypes.Success;

    • serverErrors: string[] – после отправки сообщения на сервер, хорошим тоном является проверка валидности формы и на сервере. Как следствие сервер может вернуть ошибки финальной проверки формы, и именно для таких этих ошибок предназначается массив serverErrors. Ключевой особенность serverErrors – является автоматическая очистка валидационных сообщений при потере фокуса с поля к которому были присвоены серверные ошибки, а также очистка серверных ошибок осуществляется если поле было изменено.
    • onChange: Delegate – кроме стандартного механизма mobxreaction можно использовать delegate и добавить к нему callback функцию, которая вызовется при изменении данных.
    • setDirty(dirty: boolean): void – метод позволят изменить значение полей pristine / dirty
    • setTouched(touched: boolean): void; – метод позволят изменить значение полей untouched / touched
    • dispose(): void; – обязателен к вызову в componentWillUnmount контрола отвечающего за страницу.


    Это были общие поля для всех контролов, но каждый контрол также имеет и уникальные для свое типа поля.
    FormControl
    • value — содержит текущее значение поля. Также данному полю можно присвоить новое значение.


    FormGroup и FormArray содержат
    • wait — метод позволяет ожидать окончания проверок всех (валидаций) в том числе и вложенных
    • allControls() — данный метод позволяет получить полный набор всех FormControl в том числе и вложенных на разных уровнях. Т.е. по факту он разворачивает многоуровневый объект FormGroup, который также может содержать в себе FormGroup, в один большой список состоящий только из FormControl. Данный функцонал потребуется, если мы хотим найти первый невалидный элемент и поставить на него фокус.
      код, в таком случае будет выглядеть так:
      await this.form.wait();
          if (this.form.invalid) {
            this.form.setTouched(true);
            const firstError = this.form.allControls().find(c => c.invalid && !!c.element);
            if (!!firstError) {
              firstError.element.focus();
            }
          }
      ...



    Валидации


    Конечно, кроме контролов, которые позволяют работать с данными, нам потребуется сами валидации. Пакет @quantumart/mobx-form-validation-kit естественно содержит ряд предустановленных валидаций, а также поддерживает создание собственный кастомных валидаций.
    Пример задания валидаций для FormControl для поля с указанием возраста.
    new FormControl<number>(
            this.userInfo.age,
            [required(), minValue(18, "Вам должно быть больше 18 лет.", ValidationEventTypes.Warning)],
            v => (this.userInfo.age = v)
          )

    Каждая валидация последними параметрами принимает:
    • Message – Валидационное сообщение.
    • eventType – уровень сообщения. Поддерживается 4 уровня сообщений.
      1. Error — ошибки
      2. Warning — предупреждения
      3. Info – информационные сообщения
      4. Success – сообщения о валидности. Например, можно проверить, что пароль действительно сложный.


    В пакете идет следующий набор валидаций:
    • required(… – обязательное поле
    • notEmptyOrSpaces(… – поле не пустое и не содержит одни пробелы. По факту required с учетом запрета пробелов.
    • pattern(regExp: RegExp, … – первым параметром идет регулярное выражение, которому должно соответствовать поле. Ошибка выдается, если нет соответствия паттерну.
    • invertPattern(regExp: RegExp, … – первым параметром идет регулярное выражение, которому не должно соответствовать поле. Ошибка выдается, если есть соответствия паттерну.
    • minLength(minlength: number, …. – первым параметром идет минимальная длина текста включительно. Ошибка выдается если длина меньше переданной.
    • maxLength(maxlength: number, …. – первым параметром идет максимальная длина текста включительно. Ошибка выдается если длина больше переданной.
    • absoluteLength(length: number, …. – первым параметром идет точная длина текста. Ошибка выдается если длина не соответствует заданной.
    • minValue(min: TEntity | (() => TEntity), …. – данная валидация предназначена только для чисел и дат. Ошибка устанавливается, если значение меньше указанного. Особенность валидации является возможность принимать в качестве первого параметра не только конкретное значение, но и функцию. Что означает, что если считывать значение в этой функции с @observable поля объекта, валидация сама будет перезапускаться не только при изменении поля на которое повешена валидация, но и также и при изменении «связанно поля». При этом не требуется никаких дополнительных манипуляций кроме как пометить поле с которого считывается значение как @observable.
    • maxValue(max: TEntity | (() => TEntity), …. – данная валидация предназначена только для чисел и дат. Ошибка устанавливается, если значение больше указанного. Особенность валидации является возможность принимать в качестве первого параметра не только конкретное значение, но и функцию. Что означает, что если считывать значение в этой функции с @observable поля объекта, валидация сама будет перезапускаться не только при изменении поля на которое повешена валидация, но и также и при изменении «связанно поля». При этом не требуется никаких дополнительных манипуляций кроме как пометить поле с которого считывается значение как @observable
    • notContainSpaces(… – в отличии от notEmptyOrSpaces, ошибка будет выдаваться если в значении вообще будет хоть один пробел.
    • compare(expression: (value: TEntity) => boolean(… – написание собственной функции-валидации порождает много копипастного кода, для избавления этой проблемы была разработана эта обертка. Эта валидационная функция первым параметром принимает функцию, в которую в свою очередь передается текущее значение поля. Что позволяет сделать сложную проверку. Например, расчет хеша для ИНН или номера паспорта. И после вернуть true/false. Ошибка будет отображена, если проверка вернула false.
    • isEqual(value: string… – простая проверка на соответствие строке.

    Далее описаны функции обертки, которые служат для управления потоком запуска валидаций.
    Нужно отметить, что переданный в FormControl, FormGroup, FormArray набор валидаций запускается единым скопом и по факту не имеет последовательности выполнения. Итогом работы мы будем иметь в полях errors, warnings, informationMessages, successes массивы состоявшие из объеденных в единый массив ошибок, предупреждений и т.д…
    Часто заказчик хочет увидеть лишь одну ошибку, а не все сразу. Более того, ТЗ может быть составлено так, что одна проверка выполняется только после того как прошла предыдущая.
    Для решения данной проблемы применяется обертка wrapperSequentialCheck. Ей вызов и её применение не чем не отличается от обычной функции-валидатора, но на вход она принимает массив из валидаторов которые будет запускается последовательно, т.е. следующая валидация запуститься только после того, что предыдущая прошла без ошибок.
    Второй функций оберткой является функция управления потоком валидаций. wrapperActivateValidation первым параметром принимает функцию в которой нужно прописать условия активаций валидаций. В отличии от функции activate которая передается в FormControl данная проверка рассчитана на более сложную логику. Предположим, что у нас общий билдер для целой формы FormGroup платежей, и более того на сервере есть только один метод который и принимает общий набор полей. Но вот загвоздка в том, что хоть форма и одна, в зависимости от «типа платежа» мы показываем различный набор полей пользователю. Так вот wrapperActivateValidation позволяет написать логику при которой будет осуществляться различные проверки в зависимости от типа платежа.
    Выглядеть применение оберток будет точно также, как и обычных функций.
    new FormControl(
            this.userInfo.megapole,
            [wrapperActivateValidation(() => this.info.A === 10, [
                    required(),
                    pattern(/\^d{10}$/)
            ]),
            wrapperActivateValidation(() => this.info.A === 20, [
                    wrapperSequentialCheck([
                            notContainSpaces(),
                            pattern(/\^d{20}$/)
                    ])
            ])],
            v => (this.userInfo.megapole = v)
          )
    

    Из данного примера видно, что проверки required(), pattern(/\^d{10}$/) будут осуществляться только при this.info.A === 10, а в случае если this.info.A === 20, то сработают валидации notContainSpaces(), pattern(/\^d{20}$/), кроме того эти валидации сработают последовательно, в отличии от первого случая.

    Естественно, наступит момент когда стандартного набора валидаций уже не будет хватать.
    Тогда придется писать собственные асинхронные функции. Благо это делается без особых сложностей.
    FormControl изначально затачивался на асихронные валидационые функции, которым может захотеться сходить на сервер на данными и этот ответ нужно ждать. А как следствие все валидации являются асинхронными.
    async function checkValueOnServer(control: FormControl): Promise<ValidationEvent[]> {
        if (control.value == null) {
          return [];
        }
        const result = await sendToServer(control.value);
        if (result.errorMessage) {
          return [
            {
              message: result.errorMessage,
              type: ValidationEventTypes.Error,
            },
          ];
        }
        return [];
    }

    Тут нужно обратить внимание на два объекта.
    Первый мы всегда возражающем массив. Т.е. по факту можно вернуть сразу несколько ошибочных сообщений, если вам будет угодно.
    Второй момент это возвращаемый объект, он имеет следующий набор полей.
    • key?: string — необязательное поле, позволяет задать «ключ» для конкретной валидаций. У всех базовых key уникален и совпадает с их именем. Может возникнуть желание, использовать key для рендернга списка в react, но как показала практика это плохая идея. В дальнейшем, в примере, я покажу, что лучше использовать message, а key вообще не трогать. В любом случае он есть, как и в Angunar, но вот его необходимость сведена, по факту, к 0.
    • message: string — валидационное сообщение. Обязательное поле.
    • type: ValidationEventTypes — тип сообщения.
      1. Error — ошибки
      2. Warning — предупреждения
      3. Info – информационные сообщения
      4. Success – сообщения о валидности. Например, можно проверить, что пароль действительно сложный.

    • additionalData?: any — дополнительная информация которую можно передать вместе с валидацией, если это необходимо. Это может быть какая-то дополнительная html разметка или специфичный стиль. В общем-то в any можно засунуть всё.


    Extensions


    Любая магия основывается на тривиальных вещах. И в этом случае, для работы постановки фокуса, получение изменений с полей требуется связать FormControl в конкретным полем ввода.
    Т.к. FormControl не ограничивает разработчика в типе валидируемых данных, то из-за универсальности пришлось немного пожертвовать применимостью в react элементам.
    При этом, для input и textarea удалось создать простые функции биндинга данных на элемент, для остальных компонентов, обработчику придется все же приложить минимальные усилия для подстановки данных.

    Для input биндинг элемента на FormControl (name) будет выглядеть так.
    <input type=«text» {...InputFormControl.bindActions(controls.name)} />
    Для textarea биндинг будет таким
    <textarea {...TextAreaFormControl.bindActions(controls.name)}/>

    InputFormControl.bindActions и TextAreaFormControl.bindActions принимаю два параметра:
    • formControl: FormControl — собственно FormControl на который будет приходиться биндинг. Обязательный параметр.
    • events? — Необязательный параметр, содержащий список функций, которые можно вызвать в случае необходимости их кастомизации. Суть в том, что bindActions навешивает функции-обработчики событий на Element, а как следствие, перекрытие этих событий в element приведет к неработоспособности либо FormControl-а, либо функции разработчика. Для решения этой проблемы. Мы передаем нужную кастомную функцию разработка в объект event. Сейас поддерживается следующий набор методов.
      • ref
      • onChange
      • onBlur
      • onFocus


    При использовании библиотеки вы сможете заменить, что наиболее частым вариантом создания FormControl-ов является следующая конструкция.
    this.form = new FormGroup<IUserInfo>({
          name: new FormControl(
            this.userInfo.name,
            [],
            v => (this.userInfo.name = v)
          )
        });

    Наибольшей проблемой здесь является двойное упоминание this.userInfo.name, для изначально инициализации FormControl и для записи результата. Такая связка может породить нежелательные проблемы во время копипаста и для их решения была разработана функция FormControl.for
    this.form = new FormGroup<IUserInfo>({
          name: FormControl.for(this.userInfo, 'name', [])
        });

    Как можно видеть, теперь не требуется повторять обращение к полю name name два раза. Причем, благодаря возможностям типизации в TypeScript, строка name, действительно отслеживается как поле. И если такого поля не будет в объекте userInfo — мы получим ошибку компиляции.

    Если вы дочитали досюда — вы уже герой. :)

    Пример


    Демонстрацию будем проводить на React проекте на TypeScript с использованием mobx.
    Для существующего проекта мы просто добавляем пакет.
    npm install @quantumart/mobx-form-validation-kit

    Для тестовых целей, под спойлером описано создание простого проекта
    Воспользуемся инструкцией:

    Создадим папку проекта, а после прогоним команды
    npm init –y
    npm install --save-dev webpack webpack-cli
    npm install --save react react-dom
    npm install --save-dev @types/react @types/react-dom
    npm install --save-dev typescript ts-loader source-map-loader


    Создадим tsconfig.json
    {
        "compilerOptions": {
            "outDir": "./dist/",
            "sourceMap": true,
            "noImplicitAny": true,
            "module": "commonjs",
            "target": "es6",
            "jsx": "react",
        	   "experimentalDecorators": true,
            "emitDecoratorMetadata": true
        }
    }
    

    А также прописываем файлы
    • src\components\Hello.tsx
    • src\index.tsx
    • index.html
    • src\assets\global.scss


    src\components\Hello.tsx
    import * as React from "react";
    
    export class Hello extends React.Component {
      render() {
        return (
          <h1>
    Hello from TypeScript and React!
          </h1>
        );
      }
    }
    

    src\index.tsx
    import * as React from "react";
    import * as ReactDOM from "react-dom";
    
    import { Hello } from "./components/Hello";
    
    ReactDOM.render(
        <Hello />,
        document.getElementById("example")
    );

    index.html
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8" />
            <title>Hello React!</title>
        </head>
        <body>
            <div id="example"></div>
    
            <!-- Dependencies -->
            <script src="./node_modules/react/umd/react.development.js"></script>
            <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
    
            <!-- Main -->
            <script src="./dist/main.js"></script>
        </body>
    </html>
    src\assets\global.scss
    
    .row {
      display: inline;
    }


    Далее воспользуемся наработками
    и добавим webpack-dev-server
    npm install --save-dev webpack-dev-server
    npm install --save-dev awesome-typescript-loader
    npm install --save-dev html-webpack-plugin

    И добавим webpack.config.js
    const path = require('path');
    
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      entry: './src/index.tsx',
      resolve: {
        extensions: ['.ts', '.tsx', '.js']
      },
      output: {
        path: path.join(__dirname, '/dist'),
        filename: 'bundle.min.js'
      },
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            loader: 'awesome-typescript-loader'
          },
          {
            test: /\.(scss|css)?$/,
            use: [
              { loader: 'style-loader' },
              {
                loader: 'css-loader',
                options: {
                  importLoaders: 1,
                },
              },
              { loader: 'sass-loader', options: { sourceMap: true } },
            ],
          },
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: './index.html'
        })
      ]
    }
    

    Дело за малым, добавляем mobx для React.
    "mobx": "4",
    "mobx-react": "^6.1.1",
    

    C IE10 работает только mobx, до 4 версии включительно, то лучше прописать их руками в файл package.json.
    Добавим загрузку стилей
        "style-loader": "^0.23.1",
        "css-loader": "^3.1.0",
        "node-sass": "^4.12.0",
        "sass-loader": "^7.1.0"
    

    И выполнить команду
    npm install

    Финальным аккордом собственно добавляем то, ради чего всё это затевалось.
    npm install @quantumart/mobx-form-validation-kit
    в итого получаем следующую структуру


    а package.json выглядит так
    {
      "name": "ttt",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "start": "webpack-dev-server --mode development --open",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "MIT",
      "devDependencies": {
        "@types/react": "^16.9.5",
        "@types/react-dom": "^16.9.1",
        "awesome-typescript-loader": "^5.2.1",
        "html-webpack-plugin": "^3.2.0",
        "source-map-loader": "^0.2.4",
        "ts-loader": "^6.2.0",
        "typescript": "^3.6.3",
        "webpack": "^4.41.0",
        "webpack-cli": "^3.3.9"
      },
      "dependencies": {
        "@quantumart/mobx-form-validation-kit": "^1.0.8",
        "mobx": "4",
        "mobx-react": "^6.1.1",
        "react": "^16.10.2",
        "react-dom": "^16.10.2",
        "webpack-dev-server": "^3.8.2",
        "style-loader": "^0.23.1",
        "css-loader": "^3.1.0",
        "node-sass": "^4.12.0",
        "sass-loader": "^7.1.0"
      }
    }

    Запускам проект
    npm run start

    По идее должно открыться страница с текстом
    Hello from TypeScript and React!


    Превратим компонент Hello в страницу регистрации. Для этого создадим класс RegistrationStore в новом файле RegistrationStore.ts
    src\RegistrationStore.ts
    import { observable } from "mobx";
    
    export class RegistrationStore {
      @observable
      public userInfo = {
        name: "Виталий"
      };
    }
    
    export const registrationStore = new RegistrationStore();
    

    А имеющийся файл Hello.ts, модифицируем так.
    import * as React from "react";
    import { observer } from "mobx-react";
    import { registrationStore } from "../RegistrationStore";
    
    @observer
    export class Hello extends React.Component {
      private changeName = (event: React.ChangeEvent<HTMLInputElement>) => {
        registrationStore.userInfo.name = event.target.value;
      };
      render() {
        return (
          <React.Fragment>
            <h1>Здравствуйте, {registrationStore.userInfo.name}</h1>
            <div className="row">
              <span>Имя:</span>
              <input
                type="text"
                value={registrationStore.userInfo.name}
                onChange={this.changeName}
              />
            </div>
          </React.Fragment>
        );
      }
    }
    

    В итоге получился уже функциональный компонент, со Store работающим через Mobx. Можно уже увидеть результат работы в виде динамически меняющихся текстов на странице при вводе информации в input.
    Но во всей этой красоте, есть проблема. Чем больше полей у нас добавляется, тем больше методов изменений нам придется написать. А после добавления кнопки «отправить» нужно будет не забыть проверить все поля с учетом их с видимости. И с каждым новым полем количество копипаста будет только увеличиваться, не говоря уже про сложное переиспользование кода.
    Для решения этой кучи проблем была создана
    @quantumart/mobx-form-validation-kit

    Для начала создадим небольшой компонент-обертку для визуализации ошибок.
    stc/ErrorWraper.tsx
    import * as React from "react";
    import { observer } from "mobx-react";
    import { FormControl } from "@quantumart/mobx-form-validation-kit";
    
    interface Props {
      formControl: FormControl;
    }
    
    @observer
    export class ErrorWraper extends React.Component<Props> {
      render() {
        return (
          <div>
            {this.props.children}
            {this.props.formControl.errors.map(error => (
              <span key={error.message} className="error">
                {error.message}
              </span>
            ))}
          </div>
        );
      }
    }

    В нем нет ничего сложного, просто выводим красный текст сообщений-ошибок, если они есть.

    Компонент Hello.tsx модифицируется тоже не сильно.
    Во-первых — убирается лишний метод changeName. Вместо него добавилась строка биндинга {...InputFormControl.bindActions(controls.name)}. В ней содержится все необходимые методы которые позволят реагировать на изменения данных.
    Во-вторых – мы добавили обертку для input, конечно лучше сделать отдельный компонент с input внутри, но тогда, для пояснений, потребуется немного более сложна структура.
    В-третьих – в конструктор добавлена функция которая инициализирует form в store, а, самое главное, в componentWillUnmount прописали registrationStore.form.dispose(). Без это вызова могут mobx реакции которые развешивает FromControl могут так и остаться жить до самой перезагрузки страницы.
    import * as React from "react";
    import { observer } from "mobx-react";
    import { registrationStore } from "../RegistrationStore";
    import { ErrorWraper } from "../ErrorWraper";
    import { InputFormControl } from "@quantumart/mobx-form-validation-kit";
    
    @observer
    export class Hello extends React.Component {
      constructor(props: any) {
        super(props);
        registrationStore.initForm();
      }
      componentWillUnmount() {
        registrationStore.form.dispose();
      }
      render() {
        const controls = registrationStore.form.controls;
        return (
          <React.Fragment>
            <h1>Здравствуйте, {registrationStore.userInfo.name}</h1>
            <div className="row">
              <span>Имя:</span>
              <ErrorWraper formControl={controls.name}>
                <input
                  type="text"
                  {...InputFormControl.bindActions(controls.name)}
                />
              </ErrorWraper>
            </div>
          </React.Fragment>
        );
      }
    }
    

    Дополнительным изменения подвергся и файл RegistrationStore.ts.
    Он приобрёл следующую структуру.
    Основным объектом (исходным объектом) с информацией о пользователе остался userInfo, но кроме этого появилась прослойка в виде form. Именно эта прослойка будет отвечать за валидации и за-за присвоения данных объекту userInfo.
    import { observable } from "mobx";
    import {
      FormControl,
      FormGroup,
      AbstractControls
    } from "@quantumart/mobx-form-validation-kit";
    
    interface IUserInfo extends AbstractControls {
      name: FormControl;
    }
    
    export class RegistrationStore {
      @observable
      public userInfo = {
        name: "Виталий"
      };
    
      @observable
      public form: FormGroup<IUserInfo>;
    
     public initForm(): void {
        this.form = new FormGroup<IUserInfo>({
          name: new FormControl(
            this.userInfo.name,
            [],
            v => (this.userInfo.name = v)
          )
        });
      }
    }
    export const registrationStore = new RegistrationStore();
    
Теги:
Хабы:
+8
Комментарии 6
Комментарии Комментарии 6

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн