Реализация пользовательской валидации для реактивных форм в Angular позволяет создавать специализированные проверки, которые не покрываются стандартными валидаторами. Вот полное руководство по созданию кастомных валидаторов.
Самый простой способ - создать функцию, которая возвращает объект ошибки или 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)]]
});
Для более сложных случаев можно создать класс, реализующий интерфейс 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();
});
});
Кастомные валидаторы значительно расширяют возможности Angular форм, позволяя реализовывать любые бизнес-правила валидации.