На данный момент Angular является одним из самых популярных и быстроразвивающихся фреймворков. Одна из его сильных сторон — большой встроенный инструментарий для работы с формами.
Реактивные формы — модуль, который позволяет работать с формами в реактивном стиле, создавая в компоненте дерево объектов и связывая их с шаблоном, и дает возможность подписаться из компонента на изменение в форме или отдельном контроле.
В первой части речь шла о том, как начать работать с реактивными формами. В данной статье рассмотрим валидацию форм, динамическое добавление валидации, написание собственных синхронных и ассинхронных валидаторов.
Код примеров прилагается.
Начало работы
Для работы напишем реактивную форму создания пользователей, состоящую из четырех полей:
— тип (администратор или пользователь);
— имя;
— адрес;
— пароль.
Компонент:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
userForm: FormGroup;
userTypes: string[];
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.userTypes = ['администратор', 'пользователь'];
this.initForm();
}
private initForm(): void {
this.userForm = this.fb.group({
type: null,
name: null,
address: null,
password: null
});
}
}
Шаблон:
<form class="user-form" [formGroup]="userForm">
<div class="form-group">
<label for="type">Тип пользователя:</label>
<select id="type" formControlName="type">
<option disabled value="null">выберите</option>
<option *ngFor="let userType of userTypes">{{userType}}</option>
</select>
</div>
<div class="form-group">
<label for="name">Имя пользователя:</label>
<input type="text" id="name" formControlName="name" />
</div>
<div class="form-group">
<label for="address">Адрес пользователя:</label>
<input type="text" id="address" formControlName="address" />
</div>
<div class="form-group">
<label for="password">Пароль пользователя:</label>
<input type="text" id="password" formControlName="password" />
</div>
</form>
<hr/>
<div>{{userForm.value|json}}</div>
Добавим стандартные валидаторы (работа с ними была описана в первой части):
private initForm(): void {
this.userForm = this.fb.group({
type: [null, [Validators.required]],
name: [null, [
Validators.required,
Validators.pattern(/^[A-z0-9]*$/),
Validators.minLength(3)]
],
address: null,
password: [null, [Validators.required]]
});
}
Динамическое добавление валидаторов
Иногда необходимо проверять поле только при определенных условиях. В реактивных формах можно добавлять и удалять валидаторы с помощью методов контрола.
Сделаем поле “адрес” не обязательным для администратора и обязательным для всех остальных типов пользователей.
В компоненте создаем подписку на изменение типа пользователя:
private userTypeSubscription: Subscription;
Через метод get формы получим нужный контрол и подпишемся на свойство valueChanges:
private subscribeToUserType(): void {
this.userTypeSubscription = this.userForm.get('type')
.valueChanges
.subscribe(value => console.log(value));
}
Добавим подписку в ngOnInit после инициализации формы:
ngOnInit() {
this.userTypes = ['администратор', 'пользователь'];
this.initForm();
this.subscribeToUserType();
}
И отписку в ngOnDestroy:
ngOnDestroy() {
this.userTypeSubscription.unsubscribe();
}
Добавление валидаторов к контролу происходит с помощью метода setValidators, а удаление с помощью метода clearValidators. После манипуляций с валидаторами необходимо обновить состояние контрола с помощью метода updateValueAndValidity:
private toggleAddressValidators(userType): void {
/** Контрол адреса */
const address = this.userForm.get('address');
/** Массив валидаторов */
const addressValidators: ValidatorFn[] = [
Validators.required,
Validators.min(3)
];
/** Если не админ, то добавляем валидаторы */
if (userType !== this.userTypes[0]) {
address.setValidators(addressValidators);
} else {
address.clearValidators();
}
/** Обновляем состояние контрола */
address.updateValueAndValidity();
}
Добавим метод toggleAddressValidators в подписку:
private subscribeToUserType(): void {
this.userTypeSubscription = this.userForm.get('type')
.valueChanges
.subscribe(value => this.toggleAddressValidators(value));
}
Создание кастомного валидатора
Валидатор представляет из себя функцию, на вход которой подается контрол, к которому она привязана, на выходе при ошибке валидации возвращается объект типа ValidationErrors, а при успешном прохождении валидации возвращается null.
Помимо валидаторов, предоставляемых Angular, разработчик имеет возможность написать валидатор под свои нужды.
Создадим валидатор пароля с проверкой на следующие условия:
— пароль должен содержать заглавные буквы;
— пароль должен содержать прописные буквы;
— пароль должен содержать цифры;
— длина должна быть не менее восьми символов.
/** Валидатор пароля */
private passwordValidator(control: FormControl): ValidationErrors {
const value = control.value;
/** Проверка на содержание цифр */
const hasNumber = /[0-9]/.test(value);
/** Проверка на содержание заглавных букв */
const hasCapitalLetter = /[A-Z]/.test(value);
/** Проверка на содержание прописных букв */
const hasLowercaseLetter = /[a-z]/.test(value);
/** Проверка на минимальную длину пароля */
const isLengthValid = value ? value.length > 7 : false;
/** Общая проверка */
const passwordValid = hasNumber && hasCapitalLetter && hasLowercaseLetter && isLengthValid;
if (!passwordValid) {
return { invalidPassword: 'Пароль не прошел валидацию' };
}
return null;
}
В данном примере текст при ошибке валидации один, но при желании можно сделать несколько вариантов ответов.
Добавим валидатор в форму к паролю:
private initForm(): void {
this.userForm = this.fb.group({
type: [null, [Validators.required]],
name: [null, [
Validators.required,
Validators.pattern(/^[A-z0-9]*$/),
Validators.minLength(3)]
],
address: null,
password: [null, [
Validators.required,
/** Валидатор пароля */
this.passwordValidator]
]
});
}
Получить доступ к ошибке валидации контрола можно с помощью метода getError. Добавим отображение ошибки в шаблоне:
<div class="form-group">
<label for="password">Пароль пользователя:</label>
<input type="text" id="password" formControlName="password" />
</div>
<div class="error"
*ngIf="userForm.get('password').getError('invalidPassword') && userForm.get('password').touched">
{{userForm.get('password').getError('invalidPassword')}}
</div>
Создание асинхронного валидатора
Асинхронный валидатор осуществляет валидацию с использованием данных сервера. Он представляет из себя функцию, на вход которой подается контрол, к которому она привязана, на выходе возвращается Promise или Observable (в зависимости от типа HTTP запроса) с типом ValidationErrors при ошибке и типом null при успешной валидации.
Проверим, занято ли имя пользователя.
Создадим сервис с запросом валидации (вместо http запроса будем возвращать Observable с проверкой заданного в сервисе массива пользователей):
import { Injectable } from '@angular/core';
import { ValidationErrors } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class UserValidationService {
private users: string[];
constructor() {
/** Пользователи, зарегистрированные в системе */
this.users = ['john', 'ivan', 'anna'];
}
/** Запрос валидации */
validateName(userName: string): Observable<ValidationErrors> {
/** Эмуляция запроса на сервер */
return new Observable<ValidationErrors>(observer => {
const user = this.users.find(user => user === userName);
/** если пользователь есть в массиве, то возвращаем ошибку */
if (user) {
observer.next({
nameError: 'Пользователь с таким именем уже существует'
});
observer.complete();
}
/** Если пользователя нет, то валидация успешна */
observer.next(null);
observer.complete();
}).delay(1000);
}
}
Метод delay устанавливает задержку ответа, эмулируя ассинхронность.
Теперь в компоненте создадим сам валидатор:
/** Асинхронный валидатор */
nameAsyncValidator(control: FormControl): Observable<ValidationErrors> {
return this.userValidation.validateName(control.value);
}
В данном случае валидатор возвращает вызов метода, но если сервер в случае прохождения валидации возвращает не null, то для Observable можно использовать метод map.
Асинхронный валидатор добавляется в массив описания контрола третьим элементом:
/** Инициализация формы */
private initForm(): void {
this.userForm = this.fb.group({
type: [null, [Validators.required]],
name: [null, [
Validators.required,
Validators.pattern(/^[A-z0-9]*$/),
Validators.minLength(3)],
/** Массив асинхронных валидаторов */
[this.nameAsyncValidator.bind(this)]
],
address: null,
password: [null, [
Validators.required,
this.passwordValidator]
]
});
}
В первой части говорилось, что Angular добавляет на элементы формы css классы. При использовании асинхронных валидаторов появляется еще один css класс — ng-pending, показывающий, что ответ от сервера по запросу валидации еще не получен.
Добавим в css стили, показывающие, что запрос валидации находится в обработке:
input.ng-pending{
border: 1px solid yellow;
}
Потеря контекста у валидатора
Функция валидатора, вне зависимости от того, является она синхронной или асинхронной, только добавляется к контролу, а не вызывается. Вызов происходит во время валидации вне компонента, поэтому контекст теряется, и если в валидаторе используется this, то он уже не будет указывать на компонент, и произойдет ошибка. Сохранить контекст можно, используя метод bind, или обернув валидатор в стрелочную функцию.
Ссылки
Код примера находится тут.
Более подробную информацию можно получить из официальной документации.
Все интересующиеся Angular могут присоединяться к группе русскоговорящего Angular сообщества в Telegram.