Как реализовать кастомную валидацию для реактивных форм?angular-61

Реализация пользовательской валидации для реактивных форм в Angular позволяет создавать специализированные проверки, которые не покрываются стандартными валидаторами. Вот полное руководство по созданию кастомных валидаторов.

Типы кастомных валидаторов

1. Валидатор функции

Самый простой способ - создать функцию, которая возвращает объект ошибки или null.

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

export function forbiddenNameValidator(forbiddenName: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = forbiddenName.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Использование:
this.form = this.fb.group({
  username: ['', [Validators.required, forbiddenNameValidator(/admin/i)]]
});

2. Валидатор класса

Для более сложных случаев можно создать класс, реализующий интерфейс Validator.

import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
import { Directive } from '@angular/core';

@Directive({
  selector: '[appEmailDomainValidator]',
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: EmailDomainValidatorDirective,
    multi: true
  }]
})
export class EmailDomainValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    if (!control.value) return null;

    const email = control.value as string;
    const domain = email.substring(email.lastIndexOf('@') + 1);
    const allowedDomains = ['company.com', 'partner.org'];

    return allowedDomains.includes(domain)
      ? null
      : { invalidDomain: true };
  }
}

// Использование в шаблоне:
<input formControlName="email" appEmailDomainValidator>

Кросс-полевая валидация

Для валидации, затрагивающей несколько полей формы:

export function passwordMatchValidator(formGroup: FormGroup): ValidationErrors | null {
  const password = formGroup.get('password')?.value;
  const confirmPassword = formGroup.get('confirmPassword')?.value;

  return password === confirmPassword
    ? null
    : { passwordMismatch: true };
}

// Использование:
this.form = this.fb.group({
  password: ['', Validators.required],
  confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator });

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

Для проверок, требующих API-запросов:

export function uniqueUsernameValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return userService.checkUsername(control.value).pipe(
      map(isTaken => isTaken ? { usernameTaken: true } : null),
      catchError(() => of(null)) // В случае ошибки считаем валидным
    );
  };
}

// Использование:
this.form = this.fb.group({
  username: ['', {
    validators: [Validators.required],
    asyncValidators: [uniqueUsernameValidator(this.userService)],
    updateOn: 'blur'
  }]
});

Динамическое изменение валидаторов

Изменение валидаторов во время работы приложения:

toggleExtraValidation() {
  const addressControl = this.form.get('address');
  const newValidators = this.extraValidation
    ? [Validators.required, Validators.minLength(10)]
    : [Validators.required];

  addressControl?.setValidators(newValidators);
  addressControl?.updateValueAndValidity();
}

Отображение кастомных ошибок

Шаблон для отображения пользовательских ошибок:

<div *ngIf="form.get('username').errors?.forbiddenName">
  Имя '{{form.get('username').errors?.forbiddenName.value}}' запрещено
</div>

<div *ngIf="form.get('email').errors?.invalidDomain">
  Допустимы только корпоративные email-адреса
</div>

<div *ngIf="form.errors?.passwordMismatch">
  Пароли не совпадают
</div>

Тестирование кастомных валидаторов

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

describe('ForbiddenNameValidator', () => {
  it('should return error for admin', () => {
    const control = { value: 'Admin' } as AbstractControl;
    const result = forbiddenNameValidator(/admin/i)(control);
    expect(result).toEqual({ forbiddenName: { value: 'Admin' } });
  });

  it('should return null for valid name', () => {
    const control = { value: 'John' } as AbstractControl;
    const result = forbiddenNameValidator(/admin/i)(control);
    expect(result).toBeNull();
  });
});

Резюмируем

  1. Функции-валидаторы - простой способ для изолированных проверок
  2. Класс-валидаторы - мощный инструмент для инкапсулированной логики
  3. Кросс-полевая валидация - для проверок, затрагивающих несколько контролов
  4. Асинхронные валидаторы - для проверок с API-запросами
  5. Динамическое управление - возможность изменять правила валидации
  6. Тестирование - обязательно для сложной валидационной логики

Кастомные валидаторы значительно расширяют возможности Angular форм, позволяя реализовывать любые бизнес-правила валидации.