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

Комментарии (5)


  1. Mariik
    28.01.2018 18:12

    Спасибо, полезная статья. Что нас ждет дальше?


    1. klimentRu Автор
      28.01.2018 20:57

      Спасибо! Сейчас сказать затрудняюсь. Буду продвигать Angular в массы!


  1. svatoy
    30.01.2018 19:09

    предпочитаю использовать вот такие конструкции (возможно и bind не нужен?)
    ```
    export class ValidationService {
    static notEquals(restrictedNumber: number): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} => {
    return control.value === restrictedNumber? {'notEquals': {value: control.value}}: null;
    };
    }
    }
    ```


    1. klimentRu Автор
      30.01.2018 19:10

      Та же обертка в функцию с вынесением из компонента. Тоже хороший вариант.


  1. Methos
    31.01.2018 12:36

    Похоже, js превращается в говнище наподобие C++…