Асинхронная валидация

Асинхронная валидация применяется в тех случаях, когда корректность значения невозможно определить мгновенно. Типичные примеры — проверка уникальности логина или email на сервере, валидация промокодов, запросы к внешним API, сложные бизнес-правила, зависящие от удалённых данных. В Angular асинхронная валидация встроена в систему реактивных и шаблонных форм и реализуется через RxJS.

Асинхронные валидаторы всегда возвращают результат не сразу, а в виде потока (Observable) или промиса (Promise), что позволяет форме корректно работать в условиях сетевых задержек и отмены запросов.


Отличие синхронной и асинхронной валидации

Синхронная валидация:

  • выполняется мгновенно;
  • возвращает ValidationErrors | null;
  • не поддерживает операции ввода-вывода.

Асинхронная валидация:

  • выполняется во времени;
  • возвращает Observable<ValidationErrors | null> или Promise<ValidationErrors | null>;
  • может обращаться к серверу или другим асинхронным источникам.

Angular обрабатывает оба типа валидаторов параллельно, но статус формы зависит от завершения асинхронных проверок.


Статусы формы и контролов

При использовании асинхронных валидаторов появляются дополнительные состояния:

  • PENDING — асинхронная проверка выполняется;
  • VALID — ошибок нет;
  • INVALID — обнаружены ошибки.

Статус PENDING особенно важен для пользовательского интерфейса: он позволяет отображать индикаторы загрузки или временно блокировать отправку формы.


Создание асинхронного валидатора

Асинхронный валидатор — это функция, принимающая AbstractControl и возвращающая Observable или Promise.

Пример простого асинхронного валидатора:

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

export function forbiddenNameAsync(): (control: AbstractControl) => Observable<ValidationErrors | null> {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return of(control.value).pipe(
      delay(1000),
      map(value => value === 'admin' ? { forbiddenName: true } : null)
    );
  };
}

Ключевые особенности:

  • функция возвращает функцию;
  • результатом является поток;
  • null означает отсутствие ошибок.

Использование асинхронных валидаторов в реактивных формах

В реактивных формах асинхронные валидаторы передаются третьим аргументом в FormControl.

this.form = new FormGroup({
  username: new FormControl(
    '',
    [Validators.required],
    [forbiddenNameAsync()]
  )
});

Порядок аргументов строго фиксирован:

  1. начальное значение;
  2. массив синхронных валидаторов;
  3. массив асинхронных валидаторов.

Асинхронная валидация с использованием сервиса

На практике асинхронная валидация почти всегда связана с HTTP-запросами. Валидатор обычно использует сервис.

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  checkUsername(username: string): Observable<boolean> {
    return this.http.get<boolean>(`/api/users/check/${username}`);
  }
}

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

export function usernameExistsValidator(userService: UserService) {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }

    return userService.checkUsername(control.value).pipe(
      map(exists => exists ? { usernameExists: true } : null)
    );
  };
}

Использование:

this.form = new FormGroup({
  username: new FormControl(
    '',
    [Validators.required],
    [usernameExistsValidator(this.userService)]
  )
});

Отмена запросов и оптимизация

Angular автоматически отменяет предыдущие асинхронные проверки, если значение контрола изменилось. Это достигается благодаря механизму подписок RxJS и предотвращает устаревшие результаты.

Для снижения нагрузки рекомендуется:

  • использовать debounceTime;
  • проверять пустые значения;
  • не выполнять запросы при синтаксических ошибках.

Пример с оптимизацией:

return control.valueChanges.pipe(
  debounceTime(500),
  switchMap(value => userService.checkUsername(value)),
  map(exists => exists ? { usernameExists: true } : null),
  first()
);

Асинхронные валидаторы в шаблонных формах

В шаблонных формах асинхронные валидаторы реализуются через директивы.

@Directive({
  selector: '[usernameExists]',
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: UsernameExistsDirective,
      multi: true
    }
  ]
})
export class UsernameExistsDirective implements AsyncValidator {
  constructor(private userService: UserService) {}

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.userService.checkUsername(control.value).pipe(
      map(exists => exists ? { usernameExists: true } : null)
    );
  }
}

Использование в шаблоне:

<input name="username" ngModel usernameExists />

Обработка ошибок и отображение состояния

Асинхронные ошибки отображаются так же, как синхронные. Они попадают в объект errors контрола.

<div *ngIf="form.get('username')?.pending">
  Проверка...
</div>

<div *ngIf="form.get('username')?.errors?.['usernameExists']">
  Имя пользователя уже занято
</div>

Важно учитывать, что ошибки появляются только после завершения проверки, а во время выполнения контрол находится в состоянии PENDING.


Совмещение нескольких асинхронных валидаторов

Angular поддерживает одновременное использование нескольких асинхронных валидаторов. Все они выполняются параллельно, а результат объединяется.

new FormControl(
  '',
  [],
  [
    usernameExistsValidator(this.userService),
    anotherAsyncValidator()
  ]
);

Контрол считается валидным только в том случае, если все асинхронные валидаторы вернули null.


Влияние асинхронной валидации на отправку формы

Форма не переходит в состояние VALID, пока хотя бы один контрол находится в PENDING. Это предотвращает преждевременную отправку данных.

Проверка перед отправкой:

if (this.form.valid) {
  // отправка данных
}

При наличии активной асинхронной валидации valid будет равно false.


Типичные ошибки и ограничения

  • возврат значения вместо Observable или Promise;
  • выполнение HTTP-запросов без проверки пустого значения;
  • отсутствие обработки сетевых ошибок;
  • повторные запросы без дебаунса;
  • создание валидатора с побочными эффектами.

Асинхронная валидация должна быть чистой, детерминированной и не изменять состояние приложения напрямую.