Хабрахабр

Реактивные формы (reactive forms) Angular 5 (2+). Часть 2

На данный момент 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.

Показать больше

Похожие публикации

Кнопка «Наверх»