[JavaScript, Flutter] Пакет валидации mobx form validation kit 2.0 (TypeScript / Flutter)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Предисловие
Почти полтора года назад мы выпустили первую версию библиотеки "Новый пакет валидаций для 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
SPL
Т.е. отображение происходит через «глупые» компоненты, а бизнес логика (в том числе и валидация) зашита в Store-ах.
Компоненты будут строиться на react-хуках, просто по причине, что он более современный, но библиотека хорошо работает и при «классовом подходе».
Flutter
SPL
Несмотря на то, что MVC подход фактически означает, что widget-ы мы должны унаследовать от StatelessWidget класса, наши widget-ы будут наследниками StatefulWidget. Это допущение исходит только из рекомендаций пакета flutter_mobx который служит для визуализации изменений в widget-ах при использовании state management-а mobx. Но опять же, в наших StatefulWidget-ах фактически не будет бизнес логики.
В этот раз, я статью начну именно с совсем базовых примеров. А фактически с создания проекта.
TypeScript
SPL
Создаем базовый проект
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
SPL
Создаем базовый проект
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
SPL
Flutter
SPL
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
SPL
Изменим унаследованный интерфейс 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
SPL
Для начала нам нужно создать класс унаследованный от 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
SPL
…
<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
SPL
Для начала, т.к. клик в пустою зону не считается во 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
SPL
Flutter
SPL
Как можно заметить никаких сложны манипуляций с кодом мы не провели.
Хотя в примерах и указывается только 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
SPL
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
SPL
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
SPL
firstName: new FormControl<string>("", {
validators: [
wrapperSequentialCheck([
requiredValidator(),
minLengthValidator(2),
maxLengthValidator(5),
notContainSpacesValidator(),
]),
],
}),
Flutter
SPL
firstName: FormControl(
value: "",
options: OptionsFormControl(validators: [
wrapperSequentialCheck([
requiredValidator(),
minLengthValidator(2),
maxLengthValidator(5),
notContainSpacesValidator()
])
]))
В общем и целом мы легко можем менять набор валидаций в одном конкретном месте. И нам даже представление не нужно, чтобы понять, что откуда и куда идет.
Более того, написание своей функции валидатора не представляет большой сложности.
Давайте разберем его на примере валидации группы для периода дат.
TypeScript
SPL
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
SPL
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
SPL
…
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
SPL
…
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
• Как делать сложную валидацию с запросами на сервер и при этом всё рано будет отслеживаемой.
И еще кучу всего, что позволят из коробки пакет mobx-form-validation-kit.
Надеюсь вам было интересно, это статья стала боле информативной чем первый ей вариант.
Спасибо, что прочитали статью до конца. И я очень надеюсь, что mobx-form-validation-kit станет стандартом при использовании mobx на React (TypeScript) или Flutter.
— П.с.
Если найдете ошибки – пишите, исправим :)
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, JavaScript, Программирование] Будущее JavaScript: декораторы
- [Я пиарюсь] Кому индивидуальных тренировок на основе ИИ?
- [Разработка веб-сайтов, JavaScript, Программирование, GitHub, Планшеты] Мой опыт разработки с использованием андроид устройства
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] Taiga UI — библиотека компонентов под Angular, которую вам стоит попробовать
- [Интерфейсы, ReactJS, TypeScript] Код на React и TypeScript, который работает быстро. Доклад Яндекса
- [Программирование, Java] Jmix — будущее CUBA Platform
- [Программирование] NextJS и Create-React-App. В чем разница? (перевод)
- [CSS, JavaScript, HTML] Electron — Разработка Desktop-приложений, используя HTML, CSS и JavaScript
- [Разработка мобильных приложений, Разработка под Android] Как безболезненно мигрировать с RxJava на Kotlin Coroutines+Flow
- [Node.JS] Создаем приложение на Node.JS, Express и Typescript с Jest, Swagger, log4js и Routing-controllers
Теги для поиска: #_javascript, #_flutter, #_flutter, #_mobx, #_typescript, #_validation, #_form, #_react, #_javascript, #_flutter
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:09
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Предисловие Почти полтора года назад мы выпустили первую версию библиотеки "Новый пакет валидаций для 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). TypeScriptSPLТ.е. отображение происходит через «глупые» компоненты, а бизнес логика (в том числе и валидация) зашита в Store-ах.
Компоненты будут строиться на react-хуках, просто по причине, что он более современный, но библиотека хорошо работает и при «классовом подходе». FlutterSPLНесмотря на то, что MVC подход фактически означает, что widget-ы мы должны унаследовать от StatelessWidget класса, наши widget-ы будут наследниками StatefulWidget. Это допущение исходит только из рекомендаций пакета flutter_mobx который служит для визуализации изменений в widget-ах при использовании state management-а mobx. Но опять же, в наших StatefulWidget-ах фактически не будет бизнес логики.
В этот раз, я статью начну именно с совсем базовых примеров. А фактически с создания проекта. TypeScriptSPLСоздаем базовый проект
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, FlutterSPLСоздаем базовый проект
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 и понимаете, о чем идет речь и какие есть проблемы на формах ввода. Делать мы будем небольшую форму регистрации. TypeScriptSPLFlutterSPLMain переделаем на такой вариант
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) сверху будет отображаться введенное значение. Начнем с простого. Мы запретим оставлять Имя пустым. TypeScriptSPLИзменим унаследованный интерфейс 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> ))} … FlutterSPLДля начала нам нужно создать класс унаследованный от 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 текущее состояние поля. TypeScriptSPL…
<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)} /> Но как показала практика первый вариант в больших приложения предпочтительней, и он дает больше понимания как всё это работает. FlutterSPLДля начала, т.к. клик в пустою зону не считается во 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. И любое большое приложение подразумевает написание обертки над стандартным компонентом ввода, можно конечно каждый раз копипастить строки… Но… Не будем о грустном) В итоге, сейчас мы вынесем наше поле ввода в отдельный компонент. И начнем разбор логики валидаций. TypeScriptSPLFlutterSPLКак можно заметить никаких сложны манипуляций с кодом мы не провели. Хотя в примерах и указывается только 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) Вернемся к нашему примеру страницы регистрации. TypeScriptSPLthis.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() ], }), Собственно, отображение валидаций будет таким. FlutterSPLthis.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 позволяет написать логику при которой будут осуществляться различные проверки в зависимости от типа платежа. TypeScriptSPLfirstName: new FormControl<string>("", {
validators: [ wrapperSequentialCheck([ requiredValidator(), minLengthValidator(2), maxLengthValidator(5), notContainSpacesValidator(), ]), ], }), FlutterSPLfirstName: FormControl(
value: "", options: OptionsFormControl(validators: [ wrapperSequentialCheck([ requiredValidator(), minLengthValidator(2), maxLengthValidator(5), notContainSpacesValidator() ]) ])) В общем и целом мы легко можем менять набор валидаций в одном конкретном месте. И нам даже представление не нужно, чтобы понять, что откуда и куда идет. Более того, написание своей функции валидатора не представляет большой сложности. Давайте разберем его на примере валидации группы для периода дат. TypeScriptSPLinterface 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 []; }, ], } ), }); FlutterSPLclass 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-я страница текста и еще много, о чем еще есть рассказать или рассмотреть конкретные примеры. Но давайте осилим, лишь еще один момент, что делать, если нажать на кнопку «Отправить» Наиболее популярный вариант действия, по кнопке оправить – показать все ошибочные поля и в идеале поставить поле на фокус. TypeScriptSPL…
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(); } … } FlutterSPL…
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 • Как делать сложную валидацию с запросами на сервер и при этом всё рано будет отслеживаемой. И еще кучу всего, что позволят из коробки пакет mobx-form-validation-kit. Надеюсь вам было интересно, это статья стала боле информативной чем первый ей вариант. Спасибо, что прочитали статью до конца. И я очень надеюсь, что mobx-form-validation-kit станет стандартом при использовании mobx на React (TypeScript) или Flutter. — П.с. Если найдете ошибки – пишите, исправим :) =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:09
Часовой пояс: UTC + 5