Реактивные формы в Angular
Разработка форм в Angular остаётся трудозатратным процессом, который вызывает сложности. Разбираемся, как работать с реактивными формами.
8К открытий9К показов
Данил Сабиров
Старший Архитектор WaveAccess
Несмотря на появление фронтенд-фреймворков, разработка форм в Angular остаётся трудозатратным процессом, который вызывает сложности. Зайдите на Stackoverlow и проверьте количество запросов по «Angular forms» — увидите более 48 тысяч. Почему так много? На мой взгляд, одна из проблем — два подхода реализации форм в Angular из коробки. Это шаблонный подход (Template-Driven Form) и реактивные формы (Reactive Form). Новички могут смешивать их в коде — отсюда и значительная часть вопросов.
Для создания форм в Angular я рекомендую Reactive Forms. Так можно использовать реактивную парадигму программирования, что более естественно для языка.
Работа с реактивными формами
Кратко работу с реактивными формами можно описать так: вы явно описываете форму в контроллере, включая валидацию и маппите свою модель данных на модель формы. А все необходимые операции с данными производите явно через методы формы. Также можно зарефакторить переиспользуемую логику. Модель формы описывается отдельно, благодаря этому модель данных останется неизменной во время работы пользователей с формой.
Для наглядности возьмём пример по Template-Driven Form из туториала Tour of Hero и перепишем его на реактивные формы. В результате получим следующий код контроллера hero-form.component.ts:
Первое на что стоит обратить внимание: помимо модели данных, мы создали объект класса FormGroup
. С ним мы сможем описать модель представления формы и затем передать ее непосредственно форме.
Из примера выше, можно заметить, что наша FormGroup
создается через другой класс FormBuilder
. По сути, это класс-помощник, позволяющий создать объекты формы: FormGroup
, FormControl
и FormArray
. К тому же наша модель формы сейчас почти идентично повторяет модель данных model
.
Операция маппинга модели данных на модель формы (и в последующем обратный маппинг) требует дополнительный код. Но несмотря на это нашего подхода есть один очень большой плюс. Модель данных на время работы с формой остаётся неизменной. Это позволяет сделать логику более гибкой. Плюс к вашему коду будет проще написать тесты. Замечу, что в своём примере, я пока намеренно не маппил модель данных на модель формы, чтобы осветить этот вопрос подробнее чуть ниже.
Примечание Несмотря на то, что в Template-Driven Form работа с формой происходит с директивой ngForm, «под капотом» она тоже неявно создаст объект FormGroup (советую для общего понимания ознакомится с исходниками).
Теперь рассмотрим код получившегося шаблона hero-form.component.html:
Первое, что замечаем — это передача нашей модели формы heroForm
директиве [formGroup]
. Angular для инициализации реактивной формы использует директивной подход с помощью селектора [formGroup]
, тем самым создавая объект FormGroupDirective c обязательным параметром типа FormGroup
. Второе, на что стоит обратить внимание — мы подписываемся на событие ngSubmit
, которое вызовется в случае сабмита формы (в нашем случае по нажатию по кнопке Submit).
Третий пункт нашей формы — это атрибуты formControlName
, по которым сработает другая реактивная директива FormControlName. Она принимает в качестве параметра названия поля формы и создаёт объект FormControl
. А также привязывает его к родительскому FormGroup
и связывает контрол по указанному полю с html элементом.
Опционально можно описать для каждого из контролов элементы валидации. Так, в нашем примере в разметке пользователю мы показываем что поля Name
и Power
обязательные. Делается это довольно просто:
- Получаем необходимый
FormControl
с помощью методаget
формы; - Далее получаем стандартный набор состояний контрола:
valid
,invalid
,pending
,disabled
,enabled
,dirty
,pristine
,touched
,untouched
(см. более подробно официальное API); - Реализуем нужное поведение элемента валидации, поверяя необходимые свойства контрола.
В качестве финального шага, проверяем факт валидности нашей формы с помощью valid
. И не даём засабмитить форму до тех пор, пока форма не валидна.
Если запустить код проекта, мы увидим, что форма пустая. Дело как раз в том, что мы незамаппили нашу модель. Попробуем сделать это через метод setValue
:
Вместо того, чтобы увидеть нашу форму в браузере, в консоли получим ошибку:
Проблема в том, что setValue
ищет для каждого поля модели соответствующее поле в модели формы. Мы не описали поле id
, так как оно излишне в представлении. Есть три пути решения проблемы. Самый верный — вручную замаппить модель данных на модель формы:
Также нам ничего не мешает создать у нашей формы поле id
, но в шаблоне не создавать для него контрол. Тогда мы сможем маппить нашу модель сразу на форму:
Примечание resetForm сбросит все поля в дефолтные.
Наконец, можем воспользоваться методом patchValue
, который для каждого найденного поля объекта в форме перепишет значение. А если не найдёт, то просто пропустит его. Это допустимо, если вы точно знаете, что модель данных всегда придёт со всеми необходимыми полями для формы:
Дочерние группы
Следующий пункт, который я хочу осветить — создание внутри объекта модели дочерние группы или одной FormGroup
внутри другой. В реальных проектах это необходимо, потому что формы, как правило, составные. Рассмотрим добавление подгрупп на нашем примере. Сначала создадим класс Phone
и добавим поле phone
в нашу модель данных:
phone.ts:
hero.ts:
Теперь добавляем в модель формы поле phone
:
В шаблоне добавляем разметку для отображения телефона:
Как видим, логика проставления здесь похожа, за исключением того, что мы связываем группу по директиве formGroupName
. Остальное на себя возьмёт Angular, который свяжет её с родительской формой.
На практике вложенные формы обычно вынесены в отдельные компоненты, чтобы их можно было переиспользовать. Поэтому для нашего примера создадим отдельный компонент phone. Шаблон оставим таким же, а контроллер сделаем так:
Строка viewProviders
нужна директиве FormGroupName, которая будет искать форму внутри host-элемента. В нашем случае у группы нет формы, и мы передаём провайдер, который найдёт родительскую форму. Если этого не сделать, мы получим ошибку:
Подводим итоги
Реактивные формы в Angular создаются так:
- Создаём объект формы
FormGroup
, которая описывает вашу модель данных и правила валидации; - Маппим модель данных на модель формы;
- Angular создаст реактивную форму, когда найдёт директиву
[formGroup]
с переданной ей моделью формы в шаблоне - Дочерние элементы формы в шаблоне связываются по следующим атрибутам
formGroupName
,formControlName
иformAttayName
; - Объект формы
FormGroup
содержит все необходимые методы для того, чтобы получить отдельный контрол и проверить его состояние и состояние всей формы. А также для того, чтобы установить или обновить значение контролов формы. И ещё включает событиеngSubmit
, чтобы отловить сабмит формы.
Рекомендуемые материалы
- Исходный код проекта
- Общее описание реализации форм с официального сайта Angular
- Более подробное описание Reactive Forms с примерами на сайте Angular
- Api по формам с сайта Angular
- Исходники Angular, где вы можете более подробно изучить код форм
8К открытий9К показов