В прошлых темах был описан подход Template-Driven, который концентрировался вокруг шаблона компонента: для работы с формой и ее элементами в шаблоне компонента к элементам html применялись директивы NgModel и NgForm, правила валидации задавались в тегах элементов с помощью атрибутов required и pattern. Но есть альтернативный подход - использование реактивных форм (Reactive Forms). Рассмотрим, в чем он заключается.
При подходе Reactive Forms для формы создается набор объектов FormGroup и FormControl. Сама форма и ее подсекции представляют класс FormGroup, а отдельные элементы ввода - класс FormControl. Например, базовое создание формы:
myForm : FormGroup = new FormGroup();
Добавляем в форму элементы:
myForm : FormGroup = new FormGroup({ "userName": new FormControl(), "userEmail": new FormControl(), "userPhone": new FormControl() });
Здесь определено три элемента: userName, userEmail и userPhone.
Объект FormControl может иметь различные формы определения. (Подробнее можно посмотреть в документации). В частности, в качестве первого параметра можно передавать значение по умолчанию для элемента, а в качестве второго параметра - набор валидаторов:
myForm : FormGroup = new FormGroup({ "userName": new FormControl("Tom", Validators.required), "userEmail": new FormControl("", [ Validators.required, Validators.email ]), "userPhone": new FormControl("", Validators.pattern("[0-9]{10}")) });
Здесь к элементам применяется ряд валидаторов. Валидатор Validators.required
требует обязательного наличия значения. Валидатор Validators.email
проверяет, представляет ли введенная строка электронный адрес. Валидатор Validators.pattern("[0-9]{10}")
поверяет на соответствие регулярному
выражению. Все встроенные валидаторы можно посмотреть в документации.
Если валидаторов несколько, то они заключаются в массив.
Для привязки объекта myForm к конкретному элементу формы применяется атрибут formGroup
:
<form [formGroup]="myForm" >
Кроме того, необходимо связать объекты FormControl с элементами ввода с помощью атрибута formControlName
:
<input name="name" formControlName="userName" />
Данный элемент будет связан с объектом "userName": new FormControl("Tom")
.
Теперь рассмотрим, как эти объекты будут взаимодействовать с шаблоном компонента. Для этого определим следующий компонент:
import { Component} from "@angular/core"; import { FormsModule, FormGroup, FormControl, Validators, ReactiveFormsModule} from "@angular/forms"; @Component({ selector: "my-app", standalone: true, imports: [FormsModule, ReactiveFormsModule], styles: ` div {margin: 5px 0;} .alert {color:red;} input.ng-touched.ng-invalid {border:solid red 2px;} input.ng-touched.ng-valid {border:solid green 2px;} `, template: `<form [formGroup]="myForm" novalidate (ngSubmit)="submit()"> <div> <label>Имя</label><br> <input name="name" formControlName="userName" /> @if(myForm.controls["userName"].invalid && myForm.controls["userName"].touched){ <div class="alert">Не указано имя</div> } </div> <div> <label>Email</label><br> <input name="email" formControlName="userEmail" /> @if(myForm.controls["userEmail"].invalid && myForm.controls["userEmail"].touched){ <div class="alert">Некорректный email</div> } </div> <div> <label>Телефон</label><br> <input name="phone" formControlName="userPhone" /> @if(myForm.controls["userPhone"].invalid && myForm.controls["userPhone"].touched){ <div class="alert">Некорректный номер телефона</div> } </div> <button [disabled]="myForm.invalid">Отправить </button> </form>` }) export class AppComponent { myForm : FormGroup; constructor(){ this.myForm = new FormGroup({ "userName": new FormControl("Tom", Validators.required), "userEmail": new FormControl("", [ Validators.required, Validators.email ]), "userPhone": new FormControl("", Validators.pattern("[0-9]{11}")) }); } submit(){ console.log(this.myForm); } }
Для отображения ошибок валидации здесь используется блоки div, в которых определены выражения типа
*ngIf="myForm.controls['userName'].invalid && myForm.controls['userName'].touched">
С помощью выражений myForm.controls['userName']
мы можем обратиться к нужному элементу формы и получить его состояние или значение.
В данном случае если значение поля ввода невалидно, и при этом поле ввода уже получало фокус, то отображается ошибка валидации.
Но чтобы все это заработало, необходимо импортировать модуль ReactiveFormsModule:
import { ReactiveFormsModule} from "@angular/forms"; @Component({ imports: [FormsModule, ReactiveFormsModule], // импортируем модуль ReactiveFormsModule
Кроме использования встроенных валидаторов мы также можем определять свои валидаторы. К примеру, определим в классе компонента валидатор:
export class AppComponent { myForm : FormGroup; constructor(){ this.myForm = new FormGroup({ "userName": new FormControl("Tom", [Validators.required, this.userNameValidator]), "userEmail": new FormControl("", [ Validators.required, Validators.email ]), "userPhone": new FormControl() }); } submit(){ console.log(this.myForm); } // валидатор userNameValidator(control: FormControl): {[s:string]:boolean}|null{ if(control.value==="нет"){ return {"userName": true}; } return null; } }
По сути валидатор представляет обычный метод - в данном случае метод userNameValidator
. В качестве параметра он принимает элемент формы, к
которому этот валидатор применяется, а на выходе возвращает объект, где ключ - строка, а значение равно true.
В данном случае проверяем, если значение равно строке "нет", то возвращаем объект {"userName": true}
. Значение true указывает, что
элемент формы не прошел валидацию. Если же все нормально, то возвращаем null.
Затем этот валидатор добавляется к элементу:
"userName": new FormControl("Tom", [Validators.required, this.userNameValidator])
И в случае если в поле для ввода имени будет введено значение "нет", то данное поле не пройдет валидацию:
Некоторые элементы на форме могут относиться к одному и тому же признаку. Например, в анкете пользователя могут попросить указать номера телефоно, которыми он владеет. Их может быть несколько, но они будут представлять один и тот же признак - "номера телефонов". То есть логично было бы объединить все поля для ввода номеров телефонов в массив. И в Angular мы легко можем реализовать подобную возможность с помощью класса FormArray.
Итак, изменим код компонента AppComponent следующим образом:
import { Component} from "@angular/core"; import { FormsModule, ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray} from "@angular/forms"; @Component({ selector: "my-app", standalone: true, imports: [FormsModule, ReactiveFormsModule], styles: ` div {margin: 5px 0;} .alert {color:red;} input.ng-touched.ng-invalid {border:solid red 2px;} input.ng-touched.ng-valid {border:solid green 2px;} `, template: `<form [formGroup]="myForm" novalidate (ngSubmit)="submit()"> <div> <label>Имя</label><br> <input name="name" formControlName="userName" /> @if(myForm.controls["userName"].invalid && myForm.controls["userName"].touched){ <div class="alert">Не указано имя</div> } </div> <div> <label>Email</label><br> <input name="email" formControlName="userEmail" /> @if(myForm.controls["userEmail"].invalid && myForm.controls["userEmail"].touched){ <div class="alert">Некорректный email</div> } </div> <div formArrayName="phones"> @for(phone of getFormsControls()["controls"]; track $index){ <div> <label>Телефон</label><br> <input formControlName="{{$index}}" /> </div> } </div> <button (click)="addPhone()">Добавить телефон</button> <button [disabled]="myForm.invalid">Отправить</button> </form>` }) export class AppComponent { myForm : FormGroup; constructor(){ this.myForm = new FormGroup({ "userName": new FormControl("Tom", [Validators.required]), "userEmail": new FormControl("", [ Validators.required, Validators.email ]), "phones": new FormArray([ new FormControl("+7", Validators.required) ]) }); } getFormsControls() : FormArray{ return this.myForm.controls["phones"] as FormArray; } addPhone(){ (<FormArray>this.myForm.controls["phones"]).push(new FormControl("+7", Validators.required)); } submit(){ console.log(this.myForm); } }
Теперь поля для ввода телефонных номеров представляют массив:
"phones": new FormArray([ new FormControl("+7", Validators.required) ])
Массив или FormArray хранит набор объектов FormControl. И в данном случае добавляется один такой объект.
Чтобы можно было динамически при необходимости добавлять новые объекты, в классе компонента предусмотрен метод addPhone()
:
addPhone(){ (<FormArray>this.myForm.controls["phones"]).push(new FormControl("+7", Validators.required)); }
В этой сложной конструкции мы сначала получаем объект формы через выражение this.myForm.controls["phones"]
, затем приводим его к типу
FormArray. И далее как и в обычный массив добавляем через метод push новый элемент.
Для упрощения получения массива элементов ввода здесь также определен метод getFormsControls()
, который возвращает объект FormArray:
getFormsControls() : FormArray{ return this.myForm.controls["phones"] as FormArray; }
В коде html предусматриваем вывод объектов из FormArray, возвращаемого методом getFormsControls()
, на форму с помощью конструкции @for
:
<div formArrayName="phones"> @for(phone of getFormsControls()["controls"]; track $index){ <div> <label>Телефон</label><br> <input formControlName="{{$index}}" /> </div> } </div>
При этом контейнер всех элементов ввода имеет директиву formArrayName="phones". А каждый элемент в качестве названия принимает его текущий индекс:
formControlName="{{$index}}"
.
А кнопка "Добавить телефон" позволяет добавить на форму новое поле для ввода еще одного телефонного номера:
Класс FormBuilder представляет альтернативный подход к созданию форм:
import { Component} from "@angular/core"; import { FormsModule, ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray, FormBuilder} from "@angular/forms"; @Component({ selector: "my-app", standalone: true, imports: [FormsModule, ReactiveFormsModule], styles: ` div {margin: 5px 0;} .alert {color:red;} input.ng-touched.ng-invalid {border:solid red 2px;} input.ng-touched.ng-valid {border:solid green 2px;} `, template: `<form [formGroup]="myForm" novalidate (ngSubmit)="submit()"> <div> <label>Имя</label><br> <input name="name" formControlName="userName" /> @if(myForm.controls["userName"].invalid && myForm.controls["userName"].touched){ <div class="alert">Не указано имя</div> } </div> <div> <label>Email</label><br> <input name="email" formControlName="userEmail" /> @if(myForm.controls["userEmail"].invalid && myForm.controls["userEmail"].touched){ <div class="alert">Некорректный email</div> } </div> <div formArrayName="phones"> @for(phone of getFormsControls()["controls"]; track $index){ <div> <label>Телефон</label><br> <input formControlName="{{$index}}" /> </div> } </div> <button (click)="addPhone()">Добавить телефон</button> <button [disabled]="myForm.invalid">Отправить</button> </form>` }) export class AppComponent { myForm : FormGroup; constructor(private formBuilder: FormBuilder){ this.myForm = formBuilder.group({ "userName": ["Tom", [Validators.required]], "userEmail": ["", [ Validators.required, Validators.email]], "phones": formBuilder.array([ ["+7", Validators.required] ]) }); } getFormsControls() : FormArray{ return this.myForm.controls["phones"] as FormArray; } addPhone(){ (<FormArray>this.myForm.controls["phones"]).push(new FormControl("+7", Validators.required)); } submit(){ console.log(this.myForm); } }
FormBuilder передается в качестве сервиса в конструктор. С помощью метода group()
создается объект FormGroup. Каждый элемент передается
в форму в виде обычного массива значений:
"userName": ["Tom", [Validators.required]]
Результат работы компонента будет аналогичным предыдущему.