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: Слишком сложная логика в валидаторе.
Валидатор должен быть простым и быстрым. Если логика сложная — вынесите её в отдельный сервис.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ