JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Создание пользовательских валидаторов

Создание пользовательских валидаторов

Модуль 4: Node.js, Next.js и Angular
16 уровень , 8 лекция
Открыта

1. Простые пользовательские валидаторы

Встроенные валидаторы — это как универсальный гаечный ключ: подходят для большинства типовых задач. Но в реальной жизни требования к формам бывают весьма изощрёнными. Например:

  • Пароль должен содержать спецсимволы, цифры и быть не короче 8 символов.
  • Имя пользователя не должно совпадать с чёрным списком слов (например, "admin", "root").
  • Дата рождения должна быть не раньше 1900 года и не позже текущей даты.
  • Email должен быть уникальным (проверка на сервере).
  • Поля должны быть согласованы между собой (например, "Пароль" и "Подтверждение пароля").

Встроенные валидаторы не всегда справляются с такими задачами. Вот тут и приходит время для кастомных валидаторов — функций, которые вы пишете сами!

Как устроен валидатор?

В Angular валидатор — это функция, которая получает объект AbstractControl (обычно это FormControl) и возвращает либо:

  • null, если ошибок нет (контроль прошёл валидацию),
  • объект с ошибками ({ [key: string]: any }), если валидация не прошла.

Пример: валидатор, запрещающий слово "admin" в поле имени

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function forbiddenNameValidator(forbidden: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value as string;
    if (value && value.toLowerCase() === forbidden.toLowerCase()) {
      return { forbiddenName: { value: control.value } };
    }
    return null;
  };
}

Мы возвращаем функцию-валидатор, чтобы можно было передавать параметры (например, список запрещённых имён).
Если значение совпадает с запрещённым, возвращаем объект ошибки { forbiddenName: { value: ... } }.
Если всё ок — возвращаем null.

Как подключить пользовательский валидатор к контролу?

import { FormControl } from '@angular/forms';
import { forbiddenNameValidator } from './forbidden-name.validator';

const nameControl = new FormControl('', [forbiddenNameValidator('admin')]);

В шаблоне можно отобразить ошибку:

<input [formControl]="nameControl" placeholder="Имя пользователя">
<div *ngIf="nameControl.hasError('forbiddenName')">
  Это имя запрещено!
</div>

2. Валидаторы для FormGroup: проверяем поля вместе

Иногда нужно валидировать не отдельное поле, а сразу несколько. Классический пример — "Пароль" и "Подтверждение пароля".

Пример: валидатор совпадения паролей

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export const passwordMatchValidator: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
  const password = group.get('password')?.value;
  const confirm = group.get('confirmPassword')?.value;
  return password === confirm ? null : { passwordMismatch: true };
};

Как использовать:

import { FormGroup, FormControl } from '@angular/forms';

this.form = new FormGroup({
  password: new FormControl(''),
  confirmPassword: new FormControl(''),
}, { validators: passwordMatchValidator });

В шаблоне:

<form [formGroup]="form">
  <input formControlName="password" type="password" placeholder="Пароль">
  <input formControlName="confirmPassword" type="password" placeholder="Повторите пароль">
  <div *ngIf="form.hasError('passwordMismatch') && form.touched">
    Пароли не совпадают!
  </div>
</form>

Валидатор навешивается не на отдельный контрол, а на всю форму (FormGroup), чтобы иметь доступ сразу к нескольким полям.

3. Асинхронные валидаторы

Что делать, если валидацию можно выполнить только через запрос к серверу? Например, проверить, что email уникален. Для этого нужны асинхронные валидаторы.

Как устроен асинхронный валидатор?

Асинхронный валидатор — это функция, которая возвращает либо:

  • Promise<ValidationErrors | null>, либо
  • Observable<ValidationErrors | null>.

Пример: проверка уникальности email (эмуляция запроса)

import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { delay, map } from 'rxjs/operators';

// Мнимый список занятых email
const takenEmails = ['test@example.com', 'user@site.com'];

export function uniqueEmailValidator(): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return of(takenEmails.includes(control.value)).pipe(
      delay(500), // имитируем задержку запроса
      map(isTaken => (isTaken ? { emailTaken: true } : null))
    );
  };
}

Как использовать:

import { FormControl } from '@angular/forms';

const emailControl = new FormControl(
  '',
  [], // sync validators
  [uniqueEmailValidator()] // async validators
);

В шаблоне:

<input [formControl]="emailControl" placeholder="Email">
<div *ngIf="emailControl.pending">Проверка email...</div>
<div *ngIf="emailControl.hasError('emailTaken')">Этот email уже занят!</div>

4. Практические примеры пользовательских валидаторов

Валидатор: только латинские буквы

export function latinLettersValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    return /^[a-zA-Z]+$/.test(value) ? null : { notLatin: true };
  };
}

Валидатор: минимальный возраст

export function minAgeValidator(minAge: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (!value) return null;
    const birthDate = new Date(value);
    const today = new Date();
    let age = today.getFullYear() - birthDate.getFullYear();
    const m = today.getMonth() - birthDate.getMonth();
    if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
      age--;
    }
    return age >= minAge ? null : { minAge: { requiredAge: minAge, actualAge: age } };
  };
}

Валидатор: поле не может содержать пробелы

export function noSpacesValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    return typeof control.value === 'string' && control.value.includes(' ') 
      ? { hasSpaces: true } : null;
  };
}

5. Советы по созданию и использованию пользовательских валидаторов

  • Всегда возвращайте null, если ошибок нет, и объект с ошибками — если есть.
  • Не забывайте давать уникальный ключ ошибки (forbiddenName, minAge, и т.д.), чтобы различать типы ошибок в шаблоне.
  • Для параметрических валидаторов (например, запрещённое имя, минимальный возраст) используйте функцию, возвращающую валидатор.
  • Для асинхронных проверок используйте либо Promise, либо Observable. В Angular чаще используют Observable.
  • Не делайте слишком сложную логику в валидаторе — лучше вынести бизнес-логику в сервис.
  • Асинхронный валидатор работает только на уровне FormControl — нельзя навесить асинхронный валидатор на FormGroup (на момент Angular 19).
  • Не забывайте про UX: показывайте пользователю, что идёт проверка (pending), и выводите дружелюбные сообщения об ошибках.

6. Как добавить пользовательские валидаторы в форму (пример в контексте приложения)

Допустим, у нас есть форма регистрации пользователя:

this.registerForm = new FormGroup({
  username: new FormControl(
    '',
    [Validators.required, latinLettersValidator(), forbiddenNameValidator('admin')]
  ),
  email: new FormControl(
    '',
    [Validators.required, Validators.email],
    [uniqueEmailValidator()]
  ),
  password: new FormControl(
    '',
    [Validators.required, Validators.minLength(8)]
  ),
  confirmPassword: new FormControl(
    '',
    [Validators.required]
  ),
  birthdate: new FormControl(
    '',
    [minAgeValidator(18)]
  )
}, { validators: passwordMatchValidator });

В шаблоне можно выводить ошибки так:

<input formControlName="username">
<div *ngIf="registerForm.get('username')?.hasError('notLatin')">
  Имя пользователя должно содержать только латинские буквы!
</div>
<div *ngIf="registerForm.get('username')?.hasError('forbiddenName')">
  Это имя запрещено!
</div>
<!-- и так далее для других полей -->

7. Типичные ошибки при создании пользовательских валидаторов

Ошибка №1: Не возвращаете null при успешной валидации.
Если валидатор всегда возвращает объект, даже если всё хорошо, поле всегда будет считаться невалидным. Проверяйте условие и возвращайте null, если ошибок нет!

Ошибка №2: Не уникальный ключ ошибки.
Если несколько валидаторов возвращают один и тот же ключ ошибки (например, { custom: true }), может быть путаница при выводе сообщений. Используйте уникальные имена ошибок.

Ошибка №3: Асинхронный валидатор возвращает не Observable/Promise.
Если забыть обернуть результат в of(...) или Promise.resolve(...), Angular не сможет корректно обработать результат.

Ошибка №4: Не подписываетесь на состояние pending.
Асинхронные валидаторы могут быть долгими. Не показывая пользователю, что идёт проверка, вы рискуете получить "жалобу в техподдержку".

Ошибка №5: Не используете параметры валидатора.
Пишете валидатор только для одного значения (например, только для слова "admin"), а нужно — для списка или с параметрами. Используйте функцию, возвращающую валидатор.

Ошибка №6: Слишком сложная логика в валидаторе.
Валидатор должен быть простым и быстрым. Если логика сложная — вынесите её в отдельный сервис.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ