Модуль 6 · урок 5 з 12
Асинхронні валідатори
У таблиці ліда це названо прямо: «async duplicate name check against mocked service data». Асинхронний валідатор — маленька річ, але щоб зробити її правильно, треба одночасно розуміти форми й RxJS. Тому вона й показова.
Сигнатура
type AsyncValidatorFn = (control: AbstractControl) =>
Observable<ValidationErrors | null> | Promise<ValidationErrors | null>;
Та сама логіка, що й у синхронного, тільки результат приходить у потоці. Важлива умова:
цей потік мусить завершитись, інакше контрол назавжди залишиться у стані
PENDING.
Робоча реалізація
export function uniqueCode(api: TaskApi, currentCode?: string): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const value = String(control.value ?? '').trim();
// нічого перевіряти — і нічого питати в сервера
if (!value || value === currentCode) return of(null);
return timer(400).pipe( // власний debounce усередині валідатора
switchMap(() => api.isCodeTaken(value)),
map(taken => (taken ? { codeTaken: true } : null)),
catchError(() => of(null)), // сервер лежить — не блокуємо форму
take(1), // ОБОВʼЯЗКОВО: інакше PENDING назавжди
);
};
}
// у формі — третім параметром, не другим
code: [
'',
[Validators.required, taskCode()], // синхронні
[uniqueCode(this.api, this.initial?.code)], // асинхронні
],
take(1). Angular чекає завершення потоку. Без нього статус
PENDING не зміниться ніколи, кнопка «Зберегти» лишиться заблокованою,
і причина буде зовсім неочевидна.
Debounce усередині. Асинхронні валідатори викликаються на кожну зміну значення.
Без timer кожна літера піде запитом на сервер.
Виняток для поточного значення. При редагуванні код «зайнятий» самим записом,
який ти редагуєш. Без перевірки value === currentCode форма стане
невалідною одразу після відкриття.
catchError. Якщо перевірка впала, форма не має блокуватись назавжди.
Краще пропустити збереження й отримати 409 від сервера, ніж не дати зберегти взагалі.
updateOn: коли запускати перевірку
code: new FormControl('', {
nonNullable: true,
validators: [Validators.required],
asyncValidators: [uniqueCode(this.api)],
updateOn: 'blur', // перевіряти при виході з поля, а не на кожну літеру
}),
| Значення | Коли перевіряє | Доречно |
|---|---|---|
'change' (типово) | На кожну зміну значення | Синхронні перевірки, миттєвий фідбек |
'blur' | При виході з поля | Асинхронні перевірки — найчастіший вибір |
'submit' | Лише при відправці | Довгі форми, де підказки під час набору дратують |
З updateOn: 'blur' дебаунс усередині валідатора вже не потрібен —
запит піде один раз, коли користувач перейде до наступного поля. Це найпростіший
і найпередбачуваніший варіант.
Стан PENDING в інтерфейсі
<div class="field">
<label for="code">Код</label>
<input id="code" formControlName="code">
@if (form.controls.code.pending) {
<span class="field__hint">Перевіряємо…</span>
} @else if (form.controls.code.hasError('codeTaken')) {
<span class="field__error">Такий код уже існує</span>
}
</div>
<button [disabled]="form.invalid || form.pending">Зберегти</button>
Показати «перевіряємо» — не косметика: без цього між введенням і появою помилки є
секунда, коли форма виглядає валідною, і користувач встигає натиснути «Зберегти».
Тому в кнопці й перевіряється form.pending.
PENDING.
Саме цей стан треба показати користувачу й урахувати в кнопці.