Содержание
Angular 2 предлагает разработчикам две технологии построения форм: формы на основе шаблонов (template-driven forms) и реактивные формы (reactive forms). Обе технологии входят в библиотеку @angular/forms и содержат одинаковые наборы классов элементов управления форм. При этом они предлагают разные стили и техники программирования и относятся к разным модулям: FormsModule и ReactiveFormsModule соответственно.
Reactive forms vs. template-driven forms
Реактивные формы (reactive forms) в Angular следуют реактивному стилю программирования, который предполагает явное управление потоком данных между изолированной от пользовательского интерфейса моделью данных (data model) и моделью формы (form model), непосредственно связанной с пользовательским интерфейсом и хранящей состояние и значения HTML элементов управления на экране.
В реактивных формах мы в классе компонента создаем дерево объектов, представляющих элементы управления формы, предусмотренные в Angular, и привязываем их к HTML элементам управления в шаблоне компонента так, как это будет описано ниже. Т.е. создавать и манипулировать объектами, представляющими элементы управления формы, мы будем непосредственно в классе компонента. Т.к. класс компонента имеет непосредственный доступ как к модели данных так и модели формы, мы можем передавать значения модели данных в элементы управления и извлекать отредактированные пользователем значения обратно. Таким образом компонент как бы наблюдает за изменениями в элементах управления и реагирует (react) на эти изменения.
Следуя парадигме реактивного программирования, компонент сохраняет неизменность модели данных, взаимодействуя с ней как с непосредственным источником исходных значений. Вместо того, чтобы изменять модель напрямую, компонент извлекает сделанные пользователем изменения и передает их во внешний компонент или сервис, который в свою очередь проделывает какие-либо манипуляции с этими изменениями и возвращает в исходный компонент новую модель данных, которая представляет собой новое измененное состояние модели.
Формы на основе шаблонов (template-driven forms) в этом плане ведут себя иначе. Мы размещаем HTML элементы управления (такие как, <input> или <select>) в шаблоне компонента и связываем их со свойствами модели данных в компоненте с помощью директив вроде ngModel. Мы не создаем объектов, представляющих элементы управления формы, предусмотренные в Angular. Директивы Angular’а делают это за нас. Мы не передаем значения из модели в элементы управления и обратно, Angular опять же делает это за нас. Angular обновляет изменяемую модель данных в соответствии с пользовательскими изменениями как только они происходят.
Компонент реактивной формы
Создадим класс компонента реактивной формы:
1 2 3 4 5 | import { FormControl } from '@angular/forms'; export class ReactiveFormComponent { name = new FormControl(); } |
В первой строке мы импортировали директиву FormControl, которая позволяет непосредственно создавать и управлять экземпляром FormControl.
В 4 строке мы создаем экземпляр FormControl с именем name. Он будет привязан в шаблоне к HTML элементу управления <input>. Конструктор FormControl может принимать три необязательных аргумента: начальное значение, массив валидаторов и массив асинхронных валидаторов.
Теперь создадим шаблон компонент:
1 | <input [formControl]="name"> |
Здесь чтобы связать <input> с экземпляром FormControl мы используем [formControl]="name".
И чтобы все это работало, нам необходимо импортировать ReactiveFormsModule в наше приложение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { ReactiveFormComponent } from './reactive-form.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ AppComponent, ReactiveFormComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } |
Основные классы реактивных форм
К основным классам реактивных форм можно отнести:
- AbstractControl абстрактный базовый класс для трех производных: FormControl, FormGroup и FormArray. Он реализует их общие методы и свойства
- FormControl отслеживает значение и валидность отдельного элемента управления. Он соответствует HTML элементам управления форм, таким как <input> или <select>
- FormGroup отслеживает значение и валидность группы экземпляров AbstractControl. Дочерние элементы управления являются свойствами группы. Например, форма в компоненте будет являться экземпляром FormGroup
- FormArray отслеживает значение и валидность массива экземпляров AbstractControl
FormGroup
Обычно несколько FormControl объединяют внутри родительской FormGroup:
1 2 3 4 5 6 7 8 | import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm = new FormGroup ({ name: new FormControl() }); } |
Шаблон компонента теперь будет выглядеть так:
1 2 3 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> </form> |
Мы поместили отдельный <input> внутри элемента <form>. Атрибут novalidate запрещает браузеру выполнять стандартную HTML валидацию.
Директива formGroup позволяет связать существующий экземпляр FormGroup с HTML элементом.
Как видим, синтаксис в шаблоне несколько изменился: если раньше, чтоб связать отдельный <input> с экземпляром FormControl, мы использовали [formControl]="name", то теперь для связи элемента внутри формы мы использовали formControlName="name". Т.е. для связывания отдельных элементов управления мы используем синтаксис [formControl]="name", а для связывания элемента управления внутри группы ( FormGroup) — formControlName="name".
FormBuilder
Класс FormBuilder предлагает более краткий синтаксис для создания объектов элементов формы, позволяя тем самым снизить повторяемость кода и сделать его более читаемым.
1 2 3 4 5 6 7 8 9 10 11 12 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; constructor (private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: '' }); } } |
FormBuilder.group метод-фабрика, позволяющий создать FormGroup. В качестве аргумента этот метод принимает объект, ключи которого соответствуют именам создаваемых объектов FormControl, а значения их определениям.
В примере выше объект name инициализируется пустой строкой в качестве его начального значения. Определение объекта может состоять не только из его начального значения, но и содержать валидаторы для элемента управления. В этом случае значением объекта будет массив: первый его член — начальное значение элемента управления, второй — валидатор.
1 2 3 4 5 6 7 8 9 10 11 12 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; constructor (private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ] }); } } |
Преимущества FormBuilder заметны на более развернутой форме.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; states = ['Russia', 'USA', 'Germany']; constructor(private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ], street: '', city: '', state: '', zip: '', sex: '', hasChildren: '' }); } } |
Шаблон в этом случае будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> <input formControlName="street"> <input formControlName="city"> <select formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> <input formControlName="zip"> <input type="radio" formControlName="sex" value="male"> <input type="radio" formControlName="sex" value="female"> <input type="checkbox" formControlName="hasChildren"> </form> |
Как видим объявление элементов управления в классе компонента не зависит от их представления в шаблоне (text, select, radio, checkbox). Способ привязки их в шаблоне так же не отличается.
Объекты FormControl можно группировать во вложенные FormGroup. Это позволяет повторить иерархическую структуру модели данных, а также упрощает отслеживание состояния и валидности группы связанных элементов управления.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; export class ReactiveFormComponent { reactiveForm: FormGroup; states = ['Russia', 'USA', 'Germany']; constructor(private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ], address: this.fb.group({ street: '', city: '', state: '', zip: '' }), sex: '', hasChildren: '' }); } } |
Шаблон в этом случае также должен отражать иерархичность нашей модели, обернув вложенные элементы управления в родительский элемент (например, div):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> <div formGroupName="address"> <input formControlName="street"> <input formControlName="city"> <select formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> <input formControlName="zip"> </div> <input type="radio" formControlName="sex" value="male"> <input type="radio" formControlName="sex" value="female"> <input type="checkbox" formControlName="hasChildren"> </form> |
Для доступа к отдельному FormControl внутри FormGroup можно использовать метод .get():
1 | reactiveForm.get('name').value |
Для доступа к вложенным свойствам их следует перечислить через точку:
1 | reactiveForm.get('address.street').value |
Свойства и методы AbstractControl
AbstractControl является базовым классом для FormControl, FormGroup и FormArray. Он включает следующие свойства и методы:- value возвращает значение элемента управления (только для чтения)
- validator валидатор элемента управления
- asyncValidator асинхронный валидатор
- parent возвращает родительский элемент управления (только для чтения)
- status возвращает один из 4 возможных статусов валидации элемента управления:
- VALID: элемент управления прошел все проверки валидации
- INVALID: элемент управления не прошел хотя бы одну проверку валидации
- PENDING: элемент управления находится в процессе проверки валидации
DISABLED: элемент исключен из валидации
Все статусы взаимно исключающие. Свойство доступно только для чтения
- valid возвращает true, если status === VALID (только для чтения)
- invalid возвращает true, если status === INVALID (только для чтения)
- pending возвращает true, если status === PENDING (только для чтения)
- disabled возвращает true, если status === DISABLED (только для чтения)
- enabled возвращает true, если status !== DISABLED (только для чтения)
- errors возвращает ошибки (объект ValidationErrors), сгенерированные если элемент управления не прошел валидации. Если ошибок нет возвращает null. Только для чтения
- pristine возвращает true если пользователь еще не менял значение элемента управления через пользовательский интерфейс (изменения значения через код не меняют данный статус). Только для чтения
- dirty противоположно pristine
- touched возвращает true если поле хотя бы раз получало и теряло фокус (точнее, если на нем срабатывало событие blur). Только для чтения
- untouched противоположно touched
- valueChanges (observable) генерирует событие каждый раз, когда значение элемента управления изменяется (через пользовательский интерфейс или программно), и возвращает новое значение
- statusChanges (observable) генерирует событие каждый раз, когда статус валидации поля изменяется и возвращает новый статус
- updateOn возвращает событие, после которого элемент управления изменяется. Это может быть 'change' (по умолчанию), 'blur' или 'submit'
- setValidators(newValidator) позволяет задать новые синхронные валидаторы для поля (перезаписывает существующий)
- setAsyncValidators(newValidator) позволяет задать новые асинхронные валидаторы для поля (перезаписывает существующий)
- clearValidators() удаляет все синхронные валидаторы поля
- clearAsyncValidators() удаляет все асинхронные валидаторы поля
- markAsTouched(opts) помечает элемент управления и все его дочерние элементы (при их наличии) как touched. Аргумент opts представляет собой объект, содержащий единственное свойство onlySelf, которое если равно true, то дочерние элементы не поменяют свой статус. Можно передать пустой объект.
- markAsUntouched(opts) аналогично вышеуказанному помечает элемент как untouched
- markAsDirty(opts) помечает элемент как dirty
- markAsPristine(opts) помечает элемент как pristine
- markAsPending(opts) помечает элемент как pending
- disable(opts) делает элемент управления недоступным. Это означает, что его статус будет DISABLED, он будет исключен из валидации, а его значение будет исключено из родительской модели
- enable(opts) противоположен disable(opts)
- setParent(parent) позволяет задать родительскую группу
- setValue(value) устанавливает значение элемента управления
- patchValue(value) вносит частичные изменения в значение элемента управления
- reset(value, options) сбрасывает элемент управления до начальных настроек. Оба параметра не обязательны
- updateValueAndValidity(opts) пересчитывает значение и статус валидности для элемента управления (и его предков)
- setErrors(errors, opts) при ручной валидации позволяет задать ошибки элемента управления (также меняется статус поля)
- get(path) возвращает дочерний элемент управления (рассмотрен подробнее выше)
- getError(errorCode, path) возвращает данные об ошибке, если элемент управления с указанным path содержит ошибку с указанным errorCode. В противном случае возвращает null или undefined. Если path не задан, возвращает сведения для текущего элемента управления
- hasError(errorCode, path) аналогично вышеуказанному методу проверяет наличие ошибки и возвращает true или false
- root возвращает родительский элемент коревого уровня
Модель данных и модель формы
Связь между моделью данных и моделью формы в реактивных формах разработчику необходим реализовать самому. В отличие от форм на основе шаблонов Angular автоматически этого делать не будет. Изменения вносимые пользователем через пользовательский интерфейс будут менять только модель формы, но не модель данных. Элементы управления формы не могут менять модель данных. Структуры модели формы и модели данных могут даже полностью не совпадать.
Заполнение модели формы с помощью setValue и patchValue
Задавать значения элементов управления можно не только при их инициализации, но и позже с помощью методов setValue и patchValue.
С помощью метода setValue мы можем задать значения всех элементов управления сразу передав в метод в качестве аргумента объект, который по структуре должен полностью соответствовать модели формы внутри FormGroup.
1 2 3 4 | this.reactiveForm.setValue({ name: 'Vasya', address: new Address() }); |
Метод setValue тщательно проверяет переданный объект перед тем, как присвоить значения элементам управления формы. Если переданный объект отличается по структуре от заполняемой FormGroup или в нем не хватает значений для некоторых элементов управления внутри группы, то метод setValue не примет такой объект. В этом случае метод выбросит исключение с подробным описанием ошибки (в отличие от patchValue, который не выбрасывает сообщения).
Аргументом метода setValue может быть в том числе и модель данных если она по структуре полностью идентична модели формы.
С помощью метода patchValue можно задать значение конкретных элементов управления в составе FormGroup, передав в метод объект, чьи пары ключ/значение должны соответствовать только тем элементам управления, которые необходимо вставить.
1 2 3 | this.reactiveForm.patchValue({ name: 'Vasya' }); |
Метод patchValue дает нам больше свободы, но в отличие от метода setValue он не проверяет структуру модели и не выбрасывает информативных исключений.
Сброс формы
С помощью метода .reset() можно сбросить форму. Значения элементов управления при этом очищаются, а статусы полей устанавливаются в состояние pristine.
1 | this.reactiveForm.reset(); |
В метод .reset() можно передать два необязательных параметра, задав с их помощью новые значения для элементов управления, а также статусы.
FormArray
Иногда возникает необходимость наличия в форме неопределенного числа повторяющихся полей или их групп. Так, в примере выше может быть нуль, один или несколько наборов полей, представляющих адрес. Класс FormArray как раз позволяет отображать в форме массив полей или их групп.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import { Component, Input, OnChanges } from '@angular/core'; import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Address, Hero, states } from '../data-model'; export class ReactiveFormComponent { reactiveForm: FormGroup; constructor(private fb: FormBuilder) { this.reactiveForm = this.fb.group({ name: ['', Validators.required ], addresses: this.fb.array([]), sex: '', hasChildren: '' }); } } |
Шаблон претерпит следующие изменения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <form [formGroup]="reactiveForm" novalidate> <input formControlName="name"> <div formArrayName="address"> <div *ngFor="let address of addresses.controls; let i=index" [formGroupName]="i" > <input formControlName="street"> <input formControlName="city"> <select formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> <input formControlName="zip"> </div> </div> <input type="radio" formControlName="sex" value="male"> <input type="radio" formControlName="sex" value="female"> <input type="checkbox" formControlName="hasChildren"> </form> |
Здесь прежде всего нам нужно обернуть в дополнительный <div> часть шаблона, представляющую адрес, и применить к этому <div> директиву *ngFor.
Затем мы оборачиваем <div> с директивой *ngFor в еще один внешний <div> и устанавливаем ему директиву formArrayName в значение "addresses". Это позволит установить FormArray addresses в качестве контекста для элементов управления внутри повторяющейся части шаблона.
В качестве источника повторяющихся адресов в директиве *ngFor мы используем addresses.controls, а не сам addresses. Именно свойство controls является ссылкой на массив полей.
Каждый повторяемый FormGroup нуждается в уникальном значении директивы formGroupName , в качестве которого мы используем его индекс в массиве.
Добавить новую элементы в FormArray можно с помощью методов .push(control) (добавляет элемент в конец массива) и .insert(index, control) (добавляет элемент в позицию, соответствующую индексу). Метод .removeAt(index) удаляет элемент с указанным индексом.
Отслеживание изменений
Отслеживать изменения в модели формы можно с помощью свойства valueChanges. Оно возвращает RxJS Observable.