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

Пакет валидации mobx form validation kit 2.0 (TypeScript / Flutter)

Время на прочтение 18 мин
Количество просмотров 2.9K

Предисловие


Почти полтора года назад мы выпустили первую версию библиотеки "Новый пакет валидаций для React на Mobx @quantumart/mobx-form-validation-kit"
Время шло, и библиотека корректировалась и не стояла на месте, да собственно, как и наше развитие в целом. Мы перешли на flutter, по пути попинав ReactNative, сделали еще несколько проектов. И новые задачи потребовали от нас новых и современных решений, в том числе и переработки mobx-form-validation-kit.
Сегодня я рад представить вам новую версию пакета mobx-form-validation-kit.
Для строго типизированного TypeScript
mobx-form-validation-kit 2.0
И переписанный вариант библиотеки под Flutter
flutter_mobx_form_validation_kit 2.0




Далее статья будет разделена на примеры для TypeScript и Flutter, читать вполне можно только одну версию [примеры кода], а энтузиасты обоих языков смогут сравнить подходы и применять единый подход к валидации везде.

Get Started.


Библиотеку можно применять при разных подходах к структуре кода, но я буду рассматривать библиотеку в концепции MVC (Model-View-Controller).


TypeScript
Т.е. отображение происходит через «глупые» компоненты, а бизнес логика (в том числе и валидация) зашита в Store-ах.
Компоненты будут строиться на react-хуках, просто по причине, что он более современный, но библиотека хорошо работает и при «классовом подходе».


Flutter
Несмотря на то, что MVC подход фактически означает, что widget-ы мы должны унаследовать от StatelessWidget класса, наши widget-ы будут наследниками StatefulWidget. Это допущение исходит только из рекомендаций пакета flutter_mobx который служит для визуализации изменений в widget-ах при использовании state management-а mobx. Но опять же, в наших StatefulWidget-ах фактически не будет бизнес логики.
^^^^^^^



В этот раз, я статью начну именно с совсем базовых примеров. А фактически с создания проекта.


TypeScript
Создаем базовый проект
npx create-react-app my-app --template typescript

Подключаем mobx
mobx: "^5.15.7",
mobx-react-lite: "^2.2.2",
Тут нужно сделать ремарку, что в новой 6-й версии mobx, ребята ушли от декараторов… Как следствие на 6-й версии «mobx-form-validation-kit» пока недоступна. Я думаю в ближайшее время после публикации статьи мы сделаем версию «mobx-form-validation-kit 3.0» для поддержки mobx 6+. Но API взаимодействия менять не будем, то эта статья будет актуальна.
Собственно подключаем библиотеку mobx-form-validation-kit
npm install @quantumart/mobx-form-validation-kit

В файл tsconfig.json еще прописываем.
«experimentalDecorators»: true,


Flutter
Создаем базовый проект
flutter create myapp

В зависимостях (pubspec.yaml) пропишем следующее.
dependencies:
flutter:
sdk: flutter
flutter_mobx: ^1.1.0+2
flutter_mobx_form_validation_kit:: ^2.0.0


dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.10.11
mobx_codegen: ^1.1.2

^^^^^^^



В целом, я бы не хотел подробно останавливаться на том как работает mobx или описывать как правильно организовать структуру папок. Ибо это тема для отдельной большой статьи. Я исхожу из предположения, что, раз вы читаете эту статью, то уже сталкивалиcь с mobx и понимаете, о чем идет речь и какие есть проблемы на формах ввода.
Делать мы будем небольшую форму регистрации.


TypeScript
App переделаем на такой вариант
function App() {
  return (
    <RegistrationComponent />
  );
}

Создадим компонент и Store… Пока получим вот таких два базовых файлика.



Flutter
Main переделаем на такой вариант
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(body: SafeArea(child: Registration())),
    );
  }
}

Создадим компонент и Store… Пока получим вот таких два базовых файлика.

^^^^^^^


Закончив приготовления, у нас уже получится рабочая формочка/страничка на которой при вводе значения в поле ввода (input / TextField) сверху будет отображаться введенное значение.
Начнем с простого.
Мы запретим оставлять Имя пустым.


TypeScript
Изменим унаследованный интерфейс FormRegistration от ControlsCollection. По факту здесь будет хранится только набор полей, которые мы хотим отображать / валидировать.
interface FormRegistration extends ControlsCollection {
  name: FormControl<string>;
}

Сам Store перестает хранить observable компоненты. На данный момент они нам не нужны. Вся «магия» внутри библиотеки.
export class RegistrationStore {
  public form: FormGroup<FormRegistration>;

  constructor() {
    this.form = new FormGroup<FormRegistration>({
      name: new FormControl<string>("", {
        validators: [requiredValidator()],
      }),
    });
  }
}


Наш компонент, для отображения, будет выглядеть так:
const store = new RegistrationStore();
export function RegistrationComponent() {
  return useObserver(() => (
    <div>
      <p>{store.form.controls.name.value}</p>
      <input
        type="text"
        value={store.form.controls.name.value}
        onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
          store.form.controls.name.value = event.target.value;
        }}
      />
    </div>
  ));
}

Здесь фактически ничего не меняется. Лишь значения читаются из store.form.controls.name.value.
В целом этого достаточно, что бы валидации в FormControl работали.
Давайте выведем ошибки строкой ниже созданного нами поля ввода:

…
{store.form.controls.name.errors.map((error) => (
        <p style={{ color: "#FFF" }}>{error.message}</p>
      ))}
…



Flutter
Для начала нам нужно создать класс унаследованный от ControlsCollection. По факту здесь будет храниться только набор полей, которые мы и хотим отобразить / валидировать.
class RegistrationForm extends ControlsCollection {
  final FormControl<String> name;
  RegistrationForm({this.name});
  @override
  Iterable<AbstractControl> allFields() => [this.name];
}

Основное ограничение Flutter это отсутствие reflection или mirror. Как следствие нам нужно будет override-дить метод allFields который вернет весь набор полей в классе.

Для самого store изменится так. Как видите, в данный момент даже mobx обертка для store не нужна. Вся «магия» внутри библиотеки.
class RegistrationStore {
  FormGroup<RegistrationForm> form;

  RegistrationStore() {
    this.form = FormGroup(RegistrationForm(
        name: FormControl(
            value: "",
            options: OptionsFormControl(validators: [requiredValidator()]))));
  }

  void dispose() {
    this.form.dispose();
  }
}


Сам же виджет будет выглядеть так.
class _RegistrationState extends State<Registration> {
  RegistrationStore store = RegistrationStore();

  @override
  void dispose() {
    store.dispose();
    super.dispose();
  }

  @override
  Widget build(_) => Observer(builder: (BuildContext context) {
        return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(store.form.controls.name.value,
                  style: TextStyle(fontSize: 20)),
              TextField(
                controller: store.form.controls.name.controller,
                onChanged: (String text) =>
                    store.form.controls.name.value = text,
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: "Имя",
                ),
              ),
            ]);
      });
}

Одним из основных изменений является добавление dispose. В процессе работы создается множество подписок на элементы. И от них нужно конечно избавляться если форма перестает быть нужной.
Само же поле ввода TextField стало выглядеть так.
TextField(
controller: store.form.controls.name.controller,
onChanged: (String text) =>
store.form.controls.name.value = text,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: «Имя»,
),

Прошу заметить мы не используем ни TextFormField, ни оберточный Form для визуализации, нам это не нужно. Мы используем именно базовый TextField.
TextField-у мы прописали отдельный controller:
controller: store.form.controls.name.controller
Который позволяет правильно отображать значение, заданное в Store.
И конечно изменилась функция onChanged:
onChanged: (String text) => store.form.controls.name.value = text
В целом этого достаточно, что бы валидации в FormControl работали.
Давайте выведем ошибки строкой ниже созданного нами поля ввода:

…
...store.form.controls.name.errors.map((error) => Text(
                  error.message,
                  style: TextStyle(color: Colors.red, fontSize: 15)))
…

^^^^^^^


Итогом мы получаем такую картину.


Если начать что-то вводить, то ошибка тут же пропадет.
Но естественный и привычный нам формат отображения ошибки валидации подразумевает отображение этой самой ошибки после ввода символа или после того как фокус у поля будет потерян. mobx-form-validation-kit имеется весь необходимый набор для этого.
pristine: boolean — значение в FormControl, после инициализации дефолтным значением, не изменялось.
dirty: boolean — значение в FormControl, после инициализации дефолтным значением, менялось.
untouched: boolean — для FormControl – означает, что поле не было в фокусе. Для FormGroup и FormArray означает, что ни один из вложенных FormControl-ов не был в фокусе. Значение false в этом поле означает, что фокус был не только был поставлен, но и снят с поля.
touched: boolean — Для FormControl – означает, что поле было в фокусе. Для FormGroup и FormArray означает, что один из вложенных FormControl-ов был в фокусе. Значение true в этом поле означает, что фокус был не только был поставлен, но и снят с поля.
focused: boolean — для FormControl – означает, что поле сейчас в фокусе. Для FormGroup и FormArray означает, что один из вложенных FormControl-ов сейчас в фокусе.
Но для корректной работы нужно передать FormControl текущее состояние поля.


TypeScript

…
<input
        type="text"
        value={store.form.controls.name.value}
        ref={(elment) => (props.control.element = elment)}
        onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
          store.form.controls.name.value = event.target.value;
        }}
        onBlur={(event: React.FocusEvent<HTMLInputElement>) => {
          store.form.controls.name.setTouched(true);
          store.form.controls.name.setFocused(false);
        }}
        onFocus={(event: React.FocusEvent<HTMLInputElement>) => {
          store.form.controls.name.setFocused(true);
        }}
      />
      {store.form.controls.name.touched &&
        store.form.controls.name.errors.map((error) => (
          <p style={{ color: "#F00" }}>{error.message}</p>
        ))}
…

Как мы видим, появилось большое количество кода, а именно методы
• onChange
• onBlur
• onFocus
которые имеют вполне себе стандартный подход. Дабы их не писать по сто раз, в мини-приложениях, можно воспользоваться небольшой оберткой, а именно InputFormControl или TextAreaFormControl.
В таком случае код будет выглядеть так.
<input
        type="text"
        value={store.form.controls.name.value}
        {...InputFormControl.bindActions(store.form.controls.name)}
      />

Но как показала практика первый вариант в больших приложения предпочтительней, и он дает больше понимания как всё это работает.


Flutter
Для начала, т.к. клик в пустою зону не считается во flutter потерей фокуса, мы обернем весь наш Column в InkWell.

return InkWell(
            onTap: () => FocusScope.of(context).unfocus(),
            child: Column(…));

Теперь можно добавить простой FocusNode в Widget

…
    _focusNode = FocusNode();
    _focusNode.addListener(() {
  if (!_focusNode.hasFocus) {
        store.form.controls.name.setTouched(true);
      }
      store.form.controls.name.setFocused(_focusNode.hasFocus);    }); 
…

Естественно писать однотипный код каждый раз глупо и скучно. Поэтому внутри FormControl уже вшито подобное поле focusNode.
Итого код будет таким

…
TextField(
                      focusNode: store.form.controls.name.focusNode,
                      controller: store.form.controls.name.controller,
                      onChanged: (String text) =>
                          store.form.controls.name.value = text,
                      decoration: InputDecoration(
                        border: OutlineInputBorder(),
                        labelText: "Имя",
                      )),
                  if (store.form.controls.name.touched)
                    ...store.form.controls.name.errors.map((error) => Text(
                        error.message,
                        style: TextStyle(color: Colors.red, fontSize: 15)))
…

^^^^^^^


В данном случае мы отображаем ошибку после того как поле «потрогали» (т.е. оно потеряло фокус в первый раз).
Сейчас мы подошли к моменту, что уж больно часто пишем строку store.form.controls.name.
И любое большое приложение подразумевает написание обертки над стандартным компонентом ввода, можно конечно каждый раз копипастить строки… Но… Не будем о грустном)
В итоге, сейчас мы вынесем наше поле ввода в отдельный компонент.
И начнем разбор логики валидаций.


TypeScript



Flutter


^^^^^^^


Как можно заметить никаких сложны манипуляций с кодом мы не провели.
Хотя в примерах и указывается только string, но FormControl работает с абсолютно любым типом в включая ваши собственные.

Состояние контрола


В предыдущей главе мы разобрали именно ту часть FormControl-ов которую я так сильно упростил в своей первой статье "Новый пакет валидаций для React на Mobx @quantumart/mobx-form-validation-kit
Я постарался рассказать, как их создавать и как они нативно работают.
Далее я поведаю как работать с валидациями и как создавать собственные.
Но для начала разберем структуру вложенности контроллеров и их возможности.
Библиотека mobx-form-validation-kit имеет три основных типа узлов:
FormGroup – позволяет объединять валидационные компоненты вместе. Класс типизированный, и позволяет передать в качестве generic параметра интерфейс со списком полей.
FormControl – используется для валидации конкретного поля, наиболее часто используемый класс. Класс типизированный, и в качестве generic параметра принимает тип переменной которой должен хранить.
FormArray – позволяет создавать и управлять массивом валидационных компонентов.

Сами узлы можно складывать в древовидном стиле. Поддерживается любой уровень вложенности, но обычно все начинается в FormGroup.
FormGroup
— FormControl
— FormControl
— — FormGroup
— — FormArray
— — — FormGroup
— — — — FormControl
— — FormArray
— — — FormControl

Каждый объект класса поддерживает следующий набор опций при определении:
validators: ValidatorsFunction[] – набор валидаторов.
activate: (() => boolean) | null — функция позволят включать/отключать валидации по условию (по умолчанию включено всегда). Например, валидность даты окончания услуги не нужно проверять, если не стоит галочка «Безлимитный». Как следствие, просто вписав сюда функцию которая проверив состояние observable поля отвечающего за чекбокс «Безлимитный», можно автоматически отключить все валидации привязанные к полю на проверку даты, а не прописывать эту логику в каждую из валидаций поля дата.
additionalData: any / dynamic — блок с дополнительной информацией который позволяет добавить дополнительную информацию к конкретному FormControl и использовать их в дальнейшем, например, для визуализации. Это удобно, если есть билдеры для FormControl в которых нужно захаркодить определённую информацию, а не передавать эту информацию через сложную связку данных в контролы для визуализации. Хотя точного и неоспоримого сценария применения для additionalData мы так и не смогли найти, но лучше иметь такую возможность, чем страдать без нее.
Кроме этого для FormControl есть дополнительный набор опций:
onChangeValue: UpdateValidValueHandler | null — Срабатывает всегда при изменении значения
onChangeValidValue: UpdateValidValueHandler | null — передает последнее валидное значение
callSetterOnInitialize: boolean — позволяет вызвать `onChangeValidValue` при создании `FormControl`.
callSetterOnReinitialize: boolean — позволяет вызывать `onChangeValidValue` при каждом изменении результата функции-геттера из первого аргумента.

Каждый элемент дерева поддерживает следующий набор полей
processing: boolean — в процессе анализа. mobx-form-validation-kit поддерживает асинхронные валидации, например те, что требуют запроса на сервер. Текущее состояние проверки можно узнать по данному полю.
Кроме этого поддерживается метод 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 – является автоматическая очистка валидационных сообщений при потере фокуса с поля к которому были присвоены серверные ошибки, а также очистка серверных ошибок осуществляется если поле было изменено.
setDirty(dirty: boolean): void – метод позволяет изменить значение полей pristine / dirty
setTouched(touched: boolean): void – метод позволяет изменить значение полей untouched / touched
setFocused(): void – метод позволяет изменить значение поля focused (доступно только для FormControl)
dispose(): void – обязателен к вызову в componentWillUnmount контрола отвечающего за страницу.

Примечание.
Поля valid и invalid в FormGroup и FormArray ориентируется на вложенные элементы.
И проверив самый верхний можно узнать валидность всех нижестоящих элементов формы.
НО! Узлы FormGroup и FormArray имеют cвой набор валидаций и список ошибок (errors, warnings, informationMessages, successes). Т.е. если спросить у FormGroup errors – она выдаст только свои ошибки, но не ошибки на вложенном FormControl.

Валидации


Библиотека mobx-form-validation-kit позволяет писать пользовательские валидации, но в ней присутствует и собственный набор.
requiredValidator – проверят, что значение не равно null, а для строк проверят строку на пустоту.
notEmptyOrSpacesValidator – проверяет, что значение не равно null, а для строк проверяет строку на пустоту или что она не состоит из одних пробелов.
notContainSpacesValidator — проверяет, что строка не содержит пробелов.
patternValidator – выдает ошибку, если нет соответствия паттерну
invertPatternValidator — выдает ошибку, если есть соответствие паттерну
minLengthValidator – проверяет строку на минимальную длину
maxLengthValidator – проверяет строку на максимальную длину
absoluteLengthValidator – проверяет строку на конкретную длину
isEqualValidator – проверяет на точное значение
compareValidator — обёртка для сложной проверки (ошибка, если проверка вернула false)
Вернемся к нашему примеру страницы регистрации.


TypeScript

  this.form = new FormGroup<FormRegistration>({
      firstName: new FormControl<string>("", {
        validators: [requiredValidator()],
      }),
      lastName: new FormControl<string>("", {
        validators: [requiredValidator()],
      }),
      email: new FormControl<string>("", {
        validators: [requiredValidator()],
      }),
      age: new FormControl<string>(""),
    });

Сейчас firstName проверяется только на присутствие, давайте ограничим его длину и скажем что поле не должно содержать пробелы.

      firstName: new FormControl<string>("", {
        validators: [
          requiredValidator(),
          minLengthValidator(2),
          maxLengthValidator(5),
          notContainSpacesValidator()
        ],
      }), 

Собственно, отображение валидаций будет таким.



Flutter

this.form = FormGroup(RegistrationForm(
      firstName: FormControl(
          value: "",
          options: OptionsFormControl(validators: [
            requiredValidator(),
            minLengthValidator(2),
            maxLengthValidator(5),
            notContainSpacesValidator()
          ])),
      lastName: FormControl(
          value: "",
          options: OptionsFormControl(validators: [requiredValidator()])),
      email: FormControl(
          value: "",
          options: OptionsFormControl(validators: [requiredValidator()])),
      age: FormControl(
          value: "",
          options: OptionsFormControl(validators: [requiredValidator()])),
    ));

Сейчас firstName проверяется только на присутствие, давайте ограничим его длину и скажем что поле не должно содержать пробелы.

options: OptionsFormControl(validators: [
            requiredValidator(),
            minLengthValidator(2),
            maxLengthValidator(5),
            notContainSpacesValidator()
          ])

Собственно, отображение валидаций будет таким.

^^^^^^^


Как можно заметить отработали все валидации в списке. Но часто бывает, что заказчик хочет увидеть лишь одну ошибку, а не все сразу. Более того, ТЗ может быть составлено так, что одна проверка выполняется только после того как прошла предыдущая.
Для решения данной проблемы применяется обертка wrapperSequentialCheck. Её вызов и её применение ничем не отличается от обычной функции-валидатора, но на вход она принимает массив из валидаторов которые будет запускаться последовательно, т.е. следующая валидация запустится только после того, как предыдущая прошла без ошибок.
Второй функций оберткой является функция управления потоком валидаций. wrapperActivateValidation первым параметром принимает функцию в которой нужно прописать условия активаций валидаций. В отличии от функции activate которая передается в FormControl данная проверка рассчитана на более сложную логику. Предположим, что у нас общий билдер для целой формы FormGroup платежей, и более того на сервере есть только один метод который и принимает общий набор полей. Но вот загвоздка в том, что хоть форма и одна, в зависимости от «типа платежа» мы показываем различный набор полей пользователю. Так вот wrapperActivateValidation позволяет написать логику при которой будут осуществляться различные проверки в зависимости от типа платежа.



TypeScript

firstName: new FormControl<string>("", {
        validators: [
          wrapperSequentialCheck([
            requiredValidator(),
            minLengthValidator(2),
            maxLengthValidator(5),
            notContainSpacesValidator(),
          ]),
        ],
      }),



Flutter
firstName: FormControl(
          value: "",
          options: OptionsFormControl(validators: [
            wrapperSequentialCheck([
              requiredValidator(),
              minLengthValidator(2),
              maxLengthValidator(5),
              notContainSpacesValidator()
            ])
          ]))

^^^^^^^


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


TypeScript
interface FormRange extends ControlsCollection {
  min: FormControl<Date>;
  max: FormControl<Date>;
}

interface FormRegistration extends ControlsCollection {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
  email: FormControl<string>;
  age: FormControl<string>;
  dateRange: FormGroup<FormRange>;
}


this.form = new FormGroup<FormRegistration>({
…
      dateRange: new FormGroup<FormRange>(
        {
          min: new FormControl<Date>(new Date()),
          max: new FormControl<Date>(new Date()),
        },
        {
          validators: [
            async (group: FormGroup<FormRange>): Promise<ValidationEvent[]> => {
if (group.controls.max.value < group.controls.min.value) {
                return [
                  {
                    message: 'Дата "от" больше даты "до"',
                    type: ValidationEventTypes.Error,
                  },
                ];
              }
              return [];            },
          ],
        }
      ),
    });



Flutter

class FormRange extends ControlsCollection {
  final FormControl<DateTime> min;
  final FormControl<DateTime> max;
  FormRange({this.min, this.max});
  @override
  Iterable<AbstractControl> allFields() => [this.min, this.max];
}

class RegistrationForm extends ControlsCollection {
…
  RegistrationForm(
      {this.firstName, this.lastName, this.email, this.age, this.dateRange});

  @override
  Iterable<AbstractControl> allFields() =>
      [this.firstName, this.lastName, this.email, this.age, this.dateRange];
}


this.form = FormGroup(RegistrationForm(
…
      dateRange: FormGroup<FormRange>(
          FormRange(
              min: FormControl<DateTime>(value: DateTime.now()),
              max: FormControl<DateTime>(value: DateTime.now())),
          options: OptionsFormGroup<FormRange>(validators: [
            (FormGroup<FormRange> group) async {
              if (group.controls.max.value.isBefore(group.controls.min.value)) {
                return [
                  ValidationEvent(
                    message: 'Дата "от" больше даты "до"',
                    type: ValidationEventTypes.Error,
                  )
                ];
              }
              return [];
            },
          ])),    
));

Тут нужно быть аккуратным с типами. Flutter хлебом не корми, но дай написать в ошибку, что subtype неправильный…
^^^^^^^


Как видно из примера валидация вешается не на конкретное поле [хотя так тоже можно], а вешается на всю группу целиком. Остается лишь вывести ошибки группы в интерфейсе.

Кнопка «Submit»


Устали? :) Да, я тоже… Уже идет 20-я страница текста и еще много есть о чем можно рассказать или рассмотреть конкретные примеры.
Но давайте осилим, лишь еще один момент, что делать, если нажать на кнопку «Отправить»
Наиболее популярный вариант действия, по кнопке оправить – показать все ошибочные поля и в идеале поставить поле на фокус.


TypeScript
…
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(); 
 } 
…
}



Flutter
…
await this.form.wait();
    if (this.form.invalid) {
      this.form.setTouched(true);
      final firstError = this.form.allControls().firstWhere((c) => c.invalid);
      firstError.focusNode.requestFocus();
    }
…

^^^^^^^


Основной момент здесь, это дождаться окончания работы всех валидаций.
await this.form.wait();
Дальше мы проверяем состояние наиболее общей группы на валидность, а после еще и курсор ставим на конкретное место.
И опять же как можно видеть, представление не трогается, вся логика пишется в Store, а как следствие визуализировать страницу и писать логику проверки могут совершенно разные люди. И в режиме кранча – это позволит сэкономить время.

Заключение


Понятное дело, что описанным набор возможностей библиотеки не ограничивается. Еще можно рассказать и показать примеры.
• Что валидаци отработают не только когда меняется само значение, но и когда поменялась observable переменная внутри валидации.
• Как работает метод active
• Что FormControl можно инициализовать не только конкретной переменной, но и функцией которая возвращает значение. И изменения внутри этой функции будут также отслеживаться.
• Как работать с FormArray
• Как делать сложную валидацию с запросами на сервер и при этом всё равно observable переменные в этой валидации будут отслеживаемыми.
И еще кучу всего, что позволяет из коробки пакет mobx-form-validation-kit.
Надеюсь вам было интересно, это статья стала более информативной чем первый её вариант.

Спасибо, что прочитали статью до конца. И я очень надеюсь, что mobx-form-validation-kit станет стандартом при использовании mobx на React (TypeScript) или Flutter.
— П.с.
Если найдете ошибки – пишите, исправим :)
Теги:
Хабы:
+5
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

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

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн