Связка React+MobX хорошо себя зарекомендовала при работе с формами, в то время как за реактивность модели данных в Angular обычно отвечает библиотека RxJS. Но что делать, если вы хотите воспользоваться преимуществами Angular в React или Node.js? В этой статье речь пойдет о новой библиотеке от Cloud X, которую мы разработали для того, чтобы проложить “мостик” из мира Angular, где всё богато, но дорого в мир React, где все дешево, но скудно. В этой статье я описываю применение ядра @cloudx/react-ui-kit-forms, которое отвечает за структуру модели данных, реактивность модели данных и контроль состава данных (валидацию), позволяя “скрестить” плюсы React и Angular на одном проекте.

Эта статья является продолжением первой публикации о преимуществах и недостатках React и Angular с точки зрения создания форм. Там вы найдете плюсы и минусы каждого из фреймворков, а также те мои размышления, которые натолкнули меня на идею перенести практики разработки форм на Angular для связки React+MobX. Сегодня я подробно расскажу, как устроена библиотека @cloudx/react-ui-kit-forms, которая реализует структуру хранения данных, методы манипуляций данными и точки отслеживания изменений.
Начинаем с основы
В основу библиотеки легли интерфейсы моделей форм из пакета @angular/forms, представленные тремя основными классами: AbstactControl, FormControl и FormGroup. Безусловно, в силу применяемого технологического стека от оригинала остались только основная парадигма организации структур данных, распространения событий и семантика интерфейсов классов. На этом предлагаю переходить от описательной части к практической. В качестве практического примера предлагаю рассмотреть типовую форму регистрации пользователя, состоящую из следующих полей: name, phone, zip, address, comment, agreement. Я хочу сделать форму, состоящую из нескольких секций. Для дальнейшего удобства чтения и сопровождения кода я разделю модель также на несколько секций. Пусть модель данных выглядит так:
{ personal: { name: string; phone: string; }; address: { zip: number; address: string; }; comment: string; agreement: boolean; }
По устоявшимся правилам профессиональной разработки, создадим интерфейсы, описывающие выбранную структуру данных:
interface PersonalInfo { name: FormControl<string>; phone: FormControl<string>; } interface Address { zip: FormControl<number>; street: FormControl<string>; } interface MyForm { personal: FormGroup<PersonalInfo>; address: FormGroup<Address>; comment: FormControl<string>; agreement: FormControl<boolean>; }
Следующим шагом потребуется создать в проекте MobX store:
class UserRegistrationStore { protected _form: FormGroup<MyForm>; constructor() { this._form = new FormGroup<MyForm>({ personal: new FormGroup<PersonalInfo>({ name: new FormControl<string>(''), phone: new FormControl<string>(''), }), address: new FormGroup<Address>({ zip: new FormControl<number>(1001001), street: new FormControl<string>(''), }), comment: new FormControl<string>(''), agreement: new FormControl<boolean>(false), }); } public get form(): FormGroup<MyForm> { return this._form; } }
Теперь от завершения создания модели данных нас отделяет всего лишь один шаг! Где-то в инициализации нашего приложения нужно инстанцировать стор:
export const userRegistrationStore = new UserRegistrationStore();
Всё, на этом создание модели завершено и подошло время организовать взаимодействие модели с UI слоем.
UI слой
В зависимости от выбранной библиотеки UI компонентов реализация будет немного отличаться, так что для примера я ограничусь стандартной HTML разметкой и создам простой React компонент:
const HTMLInputComponent = observer(() => { const control = userRegistrationStore.form.getControl('comment'); return ( <input type='text' value={control.value} onChange={(event) => { control.setValue(event.target.value); }} /> ); });
На этом связка слоя модели и UI слоя заканчивается, и мы получаем двустороннюю привязку данных, что позволяет получать в модели все изменения, сделанные пользователем в UI и одновременно с этим отображать пользователю все изменения, сделанные в модели. Актуальное состояние модели отражается в свойстве элемента управления value:
const formValue = userRegistrationStore.form.value;
Она имеет структуру данных, которую мы определили в самом начале.
Валидация значений
Мой рассказ был бы неполным, если бы я ничего не сказал про валидацию значений полей форм. Механизм валидации, предусмотренный в моей библиотеке, тоже имеет знакомый многим разработчикам интерфейс. Для каждого элемента управления формы можно задать набор функций, отвечающих за проверку значений или значений. Функция проверки имеет следующий интерфейс:
export interface ValidatorFn { (control: AbstractControl): string | null; } export const validator = (/* возможны аргументы */): ValidatorFn => { return (control: AbstractControl) => { if (false /* условие */) { return 'Error message'; } return null; }; };
И таких функций может быть практически неограниченное количество, в разумных, конечно пределах и передаются они в элемент управления вторым аргументом конструктора класса FormControl при создании экземпляра класса, например:
const myControl = new FormControl<string>('', [validator()]);
При решении определенных задач может потребоваться подключать или отключать определенные функции валидации, для этого предусмотрены специальные методы:
if (someCondition === false) { myControl.setValidators([]); } else { myControl.setValidators([validator ()]); }
Процесс валидации формы производится при каждом изменении значений полей модели, результаты проверки сохраняются в свойствах элементов управления valid и errors. Вариант использования может быть следующим:
if (userRegistrationStore.form.valid){ sendForm(userRegistrationStore.form.value) } else { showError(userRegistrationStore.form.errors) }
В завершение описания скажу следующие. Я специально не заострял внимание на обработке событий, генерируемых элементами модели. В свою очередь каждый элемент управления формы имеет два observable свойства – это свойства value и errors. Библиотеки mobx и mobx-react-lite предоставляют набор механизмов для работы с обозреваемыми сущностями и вопрос организации взаимодействия сущностей модели с иными сущностями зависит от выбранного архитектурного паттерна и остается в зоне ответственности конкретного разработчика.
Сколько кода нужно?
Далее я хочу провести оценку объема кода, который нужно написать разработчику чтобы реализовать форму из нашего примера на стандартных средствах React+Mobx. Представим, что мы практикуем Agile методологии в процессах разработки и конечный вариант формы мы не знаем. Будем вести разработку инкрементально и за каждую итерацию будем добавлять по одному полю или функции. Для облегчения чтения, я рассмотрю только ��аботу со стором.
Итак, погнали!
Нулевая итерация, создаем стор
Mobx+React
export interface PersonalData { name: string; phone: string; } export interface AddressData { zip: string; address: string; } export interface FormData { personal: PersonalData; address: AddressData; comment: string; agreement: boolean; } class UserRegistrationStore { constructor() { } }
ReactiveForms
interface PersonalInfo { name: FormControl<string>; phone: FormControl<string>; } interface Address { zip: FormControl<number>; street: FormControl<string>; } interface MyForm { personal: FormGroup<PersonalInfo>; address: FormGroup<Address>; comment: FormControl<string>; agreement: FormControl<boolean>; } class UserRegistrationStore { protected _form: FormGroup<MyForm>; constructor() { } }
Первая итерация. Добавлю пару полей PersonalInfo в класс стора, здесь и далее я буду отображать только вносимые изменения в классы.
MobX+React
Добавляю свойство, вношу изменения в конструктор, добавляю сеттеры.
… personal: PersonalData = { name: "", phone: "", }; … makeObservable(this, { personal: observable, setPersonalField: action, }); … setPersonalField(field: keyof PersonalData, value: string): void { this.personal[field] = value; } …
ReactiveForms
Добавляю инициализацию формы в конструктор.
… this._form = new FormGroup<MyForm>({ personal: new FormGroup<PersonalInfo>({ name: new FormControl<string>(''), phone: new FormControl<string>(''), }), }); …
Вторая итерация: Добавлю пару полей Address
Mobx+React
Добавляю свойство, вношу изменения в конструктор, добавляю сеттеры.
… address: AddressData = { zip: "", address: "", }; … makeObservable(this, { personal: observable, address: observable, setPersonalField: action, setAddressField: action, }); … setAddressField(field: keyof AddressData, value: string): void { this.address[field] = value; } …
ReactiveForms
Добавляю поля в конструктор формы.
… this._form = new FormGroup<MyForm>({ personal: new FormGroup<PersonalInfo>({ name: new FormControl<string>(''), phone: new FormControl<string>(''), }), address: new FormGroup<Address>({ zip: new FormControl<number>(1001001), street: new FormControl<string>(''), }), }); …
Третья итерация: добавлю оставшиеся поля comment и agreement
Mobx+React
Добавляю свойство, вношу изменения в конструктор, добавляю сеттеры.
… comment: string = ""; agreement: boolean = false; … makeObservable(this, { personal: observable, address: observable, comment: observable, agreement: observable, setPersonalField: action, setAddressField: action, setComment: action, setAgreement: action, }); … setComment(value: string): void { this.comment = value; } setAgreement(value: boolean): void { this.agreement = value; } …
ReactiveForms
Добавляю поля в конструктор формы
… this._form = new FormGroup<MyForm>({ personal: new FormGroup<PersonalInfo>({ name: new FormControl<string>(''), phone: new FormControl<string>(''), }), address: new FormGroup<Address>({ zip: new FormControl<number>(1001001), street: new FormControl<string>(''), }), comment: new FormControl<string>(''), agreement: new FormControl<boolean>(false), }); …
Четвертая итерация: Теперь я хочу получить все значения полей для дальнейшего использования
Mobx+React
Добавляю геттер formData , внесу изменения в конструктор
… makeObservable(this, { personal: observable, address: observable, comment: observable, agreement: observable, setPersonalField: action, setAddressField: action, setComment: action, setAgreement: action, formData: computed, }); … get formData(): FormData { return { personal: { ...this.personal }, address: { ...this.address }, comment: this.comment, agreement: this.agreement, }; } …
ReactiveForms
Здесь достаточно предоставить публичный интерфейс обращения к форме.
… public get form(): FormGroup<MyForm> { return this._form; } …
Пятая и последняя для этого примера итерация: Я хочу добавить валидацию данных, проверю, что все поля имеют значения
Mobx+React
Добавляю метод isValid, внесу изменения в конструктор
… makeObservable(this, { personal: observable, address: observable, comment: observable, agreement: observable, setPersonalField: action, setAddressField: action, setComment: action, setAgreement: action, formData: computed, isValid: computed, }); … get isValid(): boolean { const { name, phone } = this.personal; const { zip, address } = this.address; return ( name.trim() !== "" && phone.trim() !== "" && zip.trim() !== "" && address.trim() !== "" && this.agreement ); } …
ReactiveForms
Добавлять строки нет надобности, внесу изменения в существующую инициализацию
… this._form = new FormGroup<MyForm>({ personal: new FormGroup<PersonalInfo>({ name: new FormControl<string>('', [required()]), phone: new FormControl<string>('', [required()]), }), address: new FormGroup<Address>({ zip: new FormControl<number>(1001001, [required()]), street: new FormControl<string>('', [required()]), }), comment: new FormControl<string>('', [required()]), agreement: new FormControl<boolean>(false,[required()]), }); …
Я собрал в таблицу количество строк, добавленных в проект на каждой из итераций. Конечно, пример учебный, но в реальных условиях динамика будет сохраняться.


Архитектура
Перед тем, как подвести итог, я хочу рассмотреть основные строительные блоки Angular и React приложений и привести небольшой сравнительный анализ.

Как известно, в Angular, модель данных формы и разметка шаблона инкапсулированы в компоненте, что создает сильную связанность шаблона с моделью. Давайте теперь посмотрим на подобную схему для React приложения.

Как видно из диаграммы, модель данных, которую мы реализуем в store, не имеет сильной связности с разметкой шаблона, что позволяет нам как заменять визуальные компоненты без внесения каких-либо изменений в модель или store, так и использовать одну модель в разные компонентах. Таким образом, при внедрении моей библиотеки можно получить сразу две выгоды - это удобство создания реактивных моделей данных и слабую связанность слоя данных с UI слоем.
Обсудим, что дальше?
На этом хотелось бы сказать: «That’s all folks», но нет, здесь все только начинается. Впереди нас ждет история про автоматизацию разработки форм с применением ИИ и изменение как модели формы, так и UI слоя в реальном времени. Кому интересно, подписывайтесь и следите за обновлениями!
