Вольный перевод: The Ultimate Guide to Forms in Angular 2

Заметка редактора: Все написаное относится к версии Angular 2 angular-2.0.0-rc.6

Формы важны, формы сложны

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

С первого взгляда, формы могут показаться простыми: вы вставляете теги input, пользователи заполняют их и после нажатия на submit вы получаете данные. Что тут может быть сложного?

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

  • Поля формы должны изменять данные как на клиенте так и на сервер
  • Изменения часто должны отражаться еще где нибудь на странице
  • Пользовательский ввод обычно непредсказуемый, поэтому мы должны валидировать вводимые данные
  • UI должен отображать все введенные ошибки
  • Зависимые поля могут иметь достаточно сложную логику
  • Возможно нам понадобится тестирование форм без использование DOM селекторов

К счастью, Angular 2 имеет все необходимые инструменты для всего этого.

  • FormControl создает объект для работы с полями в формах
  • Validator предоставляет возможность валидировать поля
  • Observers позволяет наблюдать изменения в формах и реагировать на эти изменения

В этой статье мы опишем построение формы шаг за шагом. Мы начнем с простой формы и далее построим более сложную.

FormControl и FormGroup

Два основных объекта в формах Angular 2 FormControl и FormGroup.

FormControl

FormControl представляет одно поле ввода — это базовый кирпичик в формах Angular.

FormControl инкапсулирует значения поля и его состояния, такие как valid, dirty (changed), или наличие errors.

Для примера использование FormControl в TypeScript:

 

Для построение формы мы создаем FormControl (для группы FormControl) и затем описываем метаданные и логику.

Для во много других случаях в Angular, у нас есть класс (FormControl, в нашем случае) который подключается к DOM с атрибутом (formControl). Для примера:

 

Это создаст объект FormControl внутри контекста формы form. Мы поговорим об этом подробнее чуть ниже.

FormGroup

Большинство формы имеет более одного поля, поэтому нам нужно манипулировать несколькими FormControl. К примеру если нам понадобиться валидировать все поля ввода, будет достаточно не удобно при вводе в одном поле проходить все поля и проверять каждый FormControl. FormGroup решает эту проблему и обеспечивает интерфейс к коллекции FormControl.

Пример создания FormGroup:

 

FormGroup и FormControl имеют общий родительский класс (AbstractControl). Это означает что мы можем проверять status или value personInfo через FormControl:

 

Обратите внимание когда мы получаем value от FormGroup мы так же получаем object с парами key-value. Это намного удобнее чем проходит через массив полей FormControl.

Наша первая форма

Мы еще не касались некоторых важных моментов о которых нужно знать при создание формы. Давай те разберем их на примере следующей формы.

Скриншет формы которую мы создадим:

demo_form_sku_simple

Пример формы с Sku: Простая версия

В нашем воображаемом приложение мы создадим ввод SKU продукта.

SKU это сокращение от «stockkeeping unit». Уникальное id продукта, который необходим для отслеживания продукта на складе. Когда мы говорим о SKU, мы говорим о человеко понятном ID продукта.

Наша форма будем супер простой: одно поле ввода для sku (с описанием) и кнопкой submit.

Создание формы начнем с создания компонента Component. Если вы вспомните, нам нужно пройти три шага для создания компонента:

  • Конфигурирование аннотации @Component()
  • Создания шаблона
  • Описать функциональность в классе определяющего компонент

Загрузка FormsModule

Для использования библиотек форм прежде всего нам нужно импортировать эти библиотеки а наш NgModule.

Существует два способа использования форм в Angular: использование FormsModule или использования ReactiveFormsModule. Так как мы использует оба способа, мы импортируем обе эти библиотеки. Добавьте следующий код в ваш app.ts:

 

Это позволить использовать директивы форм в наших вьюхах. FormsModule используется при template driven директивах таки как:

  • ngModel и
  • NgForm

В то время как ReactiveFormsModule предоставляет нам такие директивы как

  • formControl and
  • ngFormGroup

…и несколько других.

Простая форма SKU: аннотация @Component

Создадим наш компонент:

 

Здесь мы определили selector demo-form-sku. Если вы вспомните, selector говорит Angular как использовать этот компонент. В нашем случае на нужно будет использовать тег demo-form-sku:

 

Простая форма SKU: шаблон

Давай определим наш шаблон template:

 

form & NgForm

Сейчас становиться все более интересней: потому что мы импортировали FormsModule, готорый создает NgForm для вьюхи. Запомните, как только сделаем директивы доступными для нашей вьюхи, они подключаться к каждому элементу которые будут соотвествовать ихнему selector.

NgForm очень удобна но не всегда очевидна : она включает тег form в наш компонент (вместо того что бы делать это явным образом, добавление ngForm как атрибут). Что означает что когда вы импортируете FormsModule, NgForm будет автоматически подключена к любому тегу <form> которая будет в вашей вьюхе. Это достаточно полезна но может вызвать путаницу, потому что это происходит за кулисами все этого процесса.

NgForm дает нам две важные функциональные возможности:

  1. FormGroup используется через атрибут ngForm
  2. Атрибут (ngSubmit)

Вы можете это увидеть в теге <form> в наше вьюхе:

 

Во первых мы определили #f="ngForm". Синтаксис #v=thing означает что мы создаем локальную переменную внутри вьюхи.

Так же мы использовали алиас ngForm, для этой вьюхи, и привезали к переменной #f. Откуда берется ngForm в первый раз? Это берется из директивы NgForm.

И какого типа объект ngForm? Это FormGroup. Что означаем что мы можем использовать f как FormGroup в нашей вьюхе. И это именно то что мы делаем в (ngSubmit).

Внимательные читатели возможно заметят что я говорил выше что NgForm автоматически подключается к тегу <form> (потому что это селектор по умолчанию NgForm), что означает мы не должны добавлять атрибут ngForm для использования NgForm. Но тогда зачем использовали атрибут ngForm? Мы сделали это сознательно. В нашем случае мы используем ngForm как атрибут для того что бы определить ссылку (_reference_). Здесь мы говорим что выражение ngForm должно быть назначено локальной переменной f.

Мы привязали действие ngSubmit используя следующий синтаксис: (ngSubmit)="onSubmit(f.value)".

  • (ngSubmit) — идет от NgForm
  • onSubmit() — будет определен в классе компонента (ниже)
  • f.valuef это FormGroup о которой мы говориле выше. И .value вернет пары key/value этого FormGroup

Иначе говоря «когда я отправлю форму, выполнится метод onSubmit моего компонента и ему передадутся значения формы как аргументы».

input & NgModel

У нашего тега input есть несколько вещей которые мы еще не касались NgModel:

 

  • class="ui form" и class="field" — это два класса исключительно по желанию . Они пренадлежать библиотеки CSS framework Semantic UI. Я их добавил только для красоты они не имеют ни какого отношения к Angular.
  • Атрибуты «for» тега label и «id» тега input для поддержки стандарта W3C standard
  • Мы добавили placeholder как «SKU», что бы была подсказка для поля input когда это поле пустое

Директива NgModel определяет selector ngModel. Это значит что мы может подключиться к нашему тегу input добавив атрибут вида: ngModel="whatever". В случае если мы используем просто ngModel это значить что мы используем атрибут без значения.

Особенности такого использования ngModel:

  1. создается одно-направленное one-way связывание
  2. при создание FormControl на нужно использовать им sku (через использование атрибута name тега input)

NgModel создает новую FormControl это автоматически добавляет к родительскому FormGroupи затем привязывается к DOM элементу новый FormControl. Это устанавливает связь между тегом input и FormControl и будет соотвествовать атрибуту name "sku".

NgModel против ngModel: в чем разница?
Вообщем то, мы используем PascalCase, как NgModelв классе компонента то есть в коде описываемого класса. В случае нижнего регистра (CamelCase) ngModel, используется только в DOM / шаблоне. Так же стоит упоминуть что NgModel и FormControl два разных объекта. NgModel это директива которую вы можете использовать в вашей вьюхе, в то время как FormControl это объект который используется для определения данных и валидации формы.
Иногда нам нужно будет использовать двух-направленное two-way связывание с ngModel как мы использовали в Angular 1. Как это сделать мы опишем ниже.

Простая форма SKU: Определям класс компонента

Сейчас определим класс:

 

У нашего класса будет только один метод: onSubmit. Это функция будет вызываться в момент отправления формы. В консоле мы сможем увидеть значения которые будут передавать наша форма.

Попробуйте это сами!

Соберем все вместе:

 

Если вы попытаетесь это запустить в браузере, то вы должны увидеть нечто подобное:

demo_form_sku_simple_submitted

Demo форма с Sku: Простая версия, Отправление

Использование FormBuilder

Создание нашего FormControl и FormGroup не явным образом используя ngForm и ngControl удобно, но не дает нам весь функционал для настройки формы. Более гибкий и более общий путь конфигурирования формы это использование FormBuilder.

FormBuilder это вспомогающий класс для построения форм. Как вы помните, формы делаются из FormControl и FormGroup а FormBuilder только поможет нам объединить это (наподобие фабрики объектов).

Давайте добавим FormBuilder к нашему примеру. Но в начале взглянем на:

  • как использовать FormBuilder в оперделение класса
  • как настраивать FormGroup в form

Реактивные формы (Reactive Forms) с FormBuilder

Для этого компонента мы будем использовать директивы formGroup и formControl что означает что нам нужно вначале импортировать соотвествующие классы:

 

 

Использование FormBuilder

Мы вставим в наш код FormBuilder через constructor:

 

Во время вставки FormBuilder мы назначим его экземпляр переменной fb.

Мы будем использовать две главные функциональности FormBuilder:

  • control — создает новый FormControl
  • group — создает новый FormGroup

Обратите внимание что мы определили новый экземпляр переменной с именем myForm.

myForm будет экземпляр FormGroup. Мы создали FormGroup вызывая fb.group(). .group принимает объект пар key-value которые описывают FormControlв этой группе.

В нашем случае, мы создали только один control sku, и назначили ему значение ["ABC123"] — что означает что мы определили значение по умолчанию для данного поля"ABC123". (Обратите внимание что это массив. Это потому что мы добавим больше конфигурационных опции позже.)

Сейчас у нас есть myForm которую мы сможем использовать во вьюхе.

Использование myForm во вьюхе

На нужно изменить нашу <form> для использования myForm. Если вы помните в последней секции мы сказали что ngForm используется автоматически когда мы используем FormsModule. Мы так же заметили что ngForm создает его собственную FormGroup. В нашем случае мы не хотим использовать FormGroup по умолчанию. Вместо этого нам нужно использовать экземпляр переменной myForm, которую мы создали с использованием FormBuilder. Так как же нам это сделать?

У Angular есть другая директива которую мы можем использовать когда нам нужно указать FormGroup: она называется formGroup и ее используют следующим образом:

 

Здесь мы говорим Angular что мы хотим использовать myForm как FormGroup для этой формы.

Помните что мы раньше говорили, когда используется FormsModule то NgForm будет автоматически применен ко всем элементам <form>. Но есть исключение: NgForm не будет применятся к <form> у который есть formGroup.

Нам так же нужно изменить onSubmit для использования в myForm вместо f.

И последнее что нам нужно сделать: привязать FormControl к тегу input. Помните что ngControl создает новый объект FormControl, и подключает его к родительскому FormGroup. Но в этом случае, мы используем FormBuilder для создания нашего FormControls.

Когда нам нужно связать существующий FormControl с полем input мы используем formControl:

Попробуйте это сами!

Полный листинг:

 

Запомните:

Для создания нового FormGroup и FormControl используется:

  • ngForm и
  • ngModel

Но для связывания с существующей FormGroup и FormControl используется:

  • formGroup and
  • formControl

Добавим проверку данных (валидация)

Пользователя не всегда вводят данный в нужном нам формате. Нам нужно сделать так что бы если кто то будет вводит данные в неправильном формате появлялось сообщение о ошибке и не был ыозможно отправить форму. Для этого используются валидаторы.

Валидаторы обеспечиваются модулем Validators и наиболее простой валидатор Validators.required что просто означает что поле обязательно к заполнению а иначе FormControl будет рассматриваться как неверное то есть invalid.

Для использования валидаторов нам нужно две вещи:

  1. Назначить валидатор объекту FormControl
  2. Проверять статус валидатора во вьюхе и соотвествующи образом реагировать на это

Для назначения валидатора объекту FormControl просто передайте его в качестве второго аргумента FormControl :

 

Или в нашем случае, так как мы используем FormBuilder на нужно использовать следующий синтаксис:

 

Теперь используем наш валидатор в вьюхе. Существует две возможности для этого:

  1. Мы можем назначить FormControl sku экземпляру класса, что более длинее в обперделения класса, но потребует меньше кода внутри вьюхи.
  2. Или мы можем найти FormControl sku из myForm во вьюхе. Это потребует меньше кода внутри оперделения класса, но больше кода внутри вьюхи.

Что бы показать разницу давайте рассмотрим оба варианта на примере:

Явное задание sku FormControl как экземпляр переменной

Скриншет нашей формы с использование валидации:

demo-form-with-validations-explicit-invalid

Demo Form с валидацией

Более гибкий путь с индивидуальным определением FormControls во вьюхе это назначение каждой FormControl как экземпляр переменно внутри определения класса. Пример подобного подхода:

 

Обратите внимание что:

  1. Мы задали sku: AbstractControl в начале класса и
  2. Мы назначили this.sku после того как мы создали myForm с FormBuilder

Теперь у нас есть ссылка на sku в любом месте вьюхи. Недостаток этого в том что нам нужно создавать экземпляр переменной для каждого поля формы. В случае больших форм это может потребовать много кода.

Теперь у нас есть наша sku с валидацией, и теперь у нас полно разных способов использовать это во вьюхе. Ниже варианты использования:

  1. Проверять все форму и отображать сообщение о ошибке
  2. Проверять каждое поле по отдельности и показывть сообщее относящее к соотвествующему полю
  3. Проверять каждое поле и окрашивать соотвествующее ошибосное поле красным без сообщений

Сообщения формы

Мы можем проверять на валидность все форму проверяя переменную myForm.valid:

 

Помните что, myForm это FormGroup и FormGroup валидно только если все его FormControlтак же валидны.

Сообщения поля

Мы так де можем отображать сообщения для соотвествующего поля если это поле FormControl не верное то есть invalid:

 

Назначение цвета полю

Я использую CSS классы Semantic UI CSS Framework .error, что значит если я добавлю класс error то <div class= "field"> будет с красным бордюром.

 

Обратите внимание что мы использовали два условия для назначения класса .error: Мы проверяем !sku.valid и sku.touched. Для того что бы ошибка появлялась только после попытки ввода пользователем.

Указание валидация

Поле формы может быть неверно по множеству причин. Нам частенько нужно отобразить различные сообщения о неверной валидации.

Для примера:

 

Обратите что hasError определяется в FormControl и FormGroup. Что означает что мы можем передать в качестве второго аргумент имя нужного нам поля указанного в FormGroup. Для примера:

 

Соберем все вместе

Полный пример кода нашей формы с валидацией FormControl и использованием экземпляра переменной:

 

Удаление экземпляра переменной sku

В примере выше мы использовали sku: AbstractControl как экземпляр переменной. Всегда ли нам нужну будет использовать создание экзепляра перменной для каждого поля или, мы можем ссылаться на поля FormControl внутри вьюхи без использования экземпляра переменной

Для этого мы можем использовать myForm.controls:

 

В этом случае мы получаем доступ к полю sku без явного создания экземпляра поля.

Нестандартная валидация

Нам частенько нужно будет делать собственные валидации полей. Давайте рассмотрим как это делается.

Что бы понять как создаются валидаторы давайте посмотрим на стандартный Validators.required из исходников Angular:

 

Валидатор:

— Принимает FormControl как поле input и

— Возвращает StringMap<string, boolean> где ключ это «error code» а значение true есди поле неверное

Создание собственного валидатора

Давай добавим требование к нашему sku. Для примера пусть все наши sku должны начинаться с 123. Для этого напишем следующий валидатор:

 

Этот валидатор вернет код ошибки invalidSku если поле (the control.value) будет не начинаться с 123.

Назначим валидатор FormControl

Добавим наш валидатор к FormControl. Однако, здесь есть маленькая проблема: у нас уже есть валидатор для sku. Как же нам добавить несколько валидаторов к одному полю?

Для этого используем Validators.compose:

 

Validators.compose оборачивает два валидатора в один и позволяет назначить их FormControl. FormControl будет верным то есть valid если будут верны оба этих валидатора.

Теперь мы можем использовать наш новый валидатор во вьюхе:

 

Наблюдения за изменениями

В нашем примере мы можем получить доступ к данным которые вводит пользователь только когда форма будет отправлена то есть вызван метод onSubmit. Но иногда нам нужно отслеживать изменения в полях.

Оба FormGroup и FormControl имеют EventEmitter поэтому для этого мы можем использовать объект observe.

EventEmitter это Observable, что означает что он должен удовлетворять спецификации для наблюдения за изменениями. Если вам интересна спецификация Observable, найдите ее здесь

Для наблюдением за полем на нужно:

  1. получить доступ к EventEmitter вызывая control.valueChanges. Затем нам нужно
  2. добавить observer используя метод .observer

Пример:

 

Здесь мы будет наблюдать за двумя событиями: изменения поля sku и изменения формы целиком.

Если мы наберем ‘kj‘ в тестовом поле то это должно отобразиться в консоле:

 

ngModel

NgModel это специальная директива: она связывает модел и форму. ngModel так же может создавать дву-напраленую связанность. По умолчанию Angular 2 использует одно-напрвленую связанность: сверху-вниз. Однако, когда ресь идет о формах, иногда возникает необходимость в двух-направленной свзяности .

Только лишь потому что вы использовали ng-model в Angular 1 в прошлом, не спешите использовать ngModel точно таким же образом. Существуют много хороших причин избегать использования дву-направленной связаности. Конечно же, ngModel может быть очень удобна, но не полагайтесь на дву-напрвленную связаность как мы делали это в Angular 1.

Давай изменим нашу форму и допустим нам нужно будет новое поле productName. Мы собираемся использовать ngModel для сохранения экземпляра переменной синхронным с вьюхой.

Определим наш класс:

 

Обратите внимание что мы используем productName: string как переменную.

Далее используем ngModel в теги input:

Синтаксим ngModel может быть для вас несколько странным: использоание квадратных и круглых скобок вокруг атрибута ngModel! Идея тут заключается в том что мы создаем дву-направленую связь.

Еще обратите внимание что мы так же используем formControl для связывания поля input с FormControl.

Давай те отобразим значение productName во вьюхе:

Скриншет результата:

demo-form-ng-model-submitted

Demo Form with ngModel

Просто!

Завершение

Формы имеют много составных частей, но Angular 2 делает работу с ними достаточно простой. Как только вы научитесь использовать FormGroup, FormControl и Validation вам станет намного проще использовать формы в Angular 2!

It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInShare on VK