Reactive Forms

Последнее обновление: 15.11.2023

В прошлых темах был описан подход 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
Data-Driven подход в Angular 17

Определение валидаторов

Кроме использования встроенных валидаторов мы также можем определять свои валидаторы. К примеру, определим в классе компонента валидатор:

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 17

Массивы элементов и FormArray

Некоторые элементы на форме могут относиться к одному и тому же признаку. Например, в анкете пользователя могут попросить указать номера телефоно, которыми он владеет. Их может быть несколько, но они будут представлять один и тот же признак - "номера телефонов". То есть логично было бы объединить все поля для ввода номеров телефонов в массив. И в 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}}".

А кнопка "Добавить телефон" позволяет добавить на форму новое поле для ввода еще одного телефонного номера:

FormArray in Angular 17

FormBuilder

Класс 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]]

Результат работы компонента будет аналогичным предыдущему.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850