Модуль 6 · урок 9 з 12
Показ помилок
«Instant user feedback and clear error messages» — критерій приймання. Тут дві задачі: не дублювати розмітку помилок у кожному полі й писати повідомлення, які щось пояснюють. Друге виявляється складнішим за перше.
Проблема дублювання
<!-- ❌ і так у кожному полі форми -->
<input formControlName="code">
@if (form.controls.code.touched && form.controls.code.invalid) {
@if (form.controls.code.hasError('required')) { <p>Обовʼязкове поле</p> }
@if (form.controls.code.hasError('format')) { <p>Невірний формат</p> }
@if (form.controls.code.hasError('codeTaken')) { <p>Код зайнятий</p> }
}
На пʼяти полях це вісімдесят рядків шаблону, у яких легко зробити помилку — і в яких точно забудеться нове правило валідації.
Один компонент помилки
@Component({
selector: 'app-field-error',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (message(); as text) {
<p class="field__error" role="alert">{{ text }}</p>
}
`,
})
export class FieldError {
control = input.required<AbstractControl>();
submitted = input(false);
readonly message = computed(() => {
const c = this.control();
if (!c.errors || (!c.touched && !this.submitted())) return null;
return firstMessage(c.errors);
});
}
const MESSAGES: Record<string, (e: any) => string> = {
required: () => 'Обовʼязкове поле',
format: () => 'Формат: KYIV-104',
codeTaken: () => 'Такий код уже існує',
minlength: e => `Мінімум ${e.requiredLength} символів`,
maxlength: e => `Не більше ${e.requiredLength} символів`,
min: e => `Не менше ${e.min}`,
max: e => `Не більше ${e.max}`,
email: () => 'Схоже на неправильну адресу',
};
// порядок важливий: спершу найзагальніше
const PRIORITY = ['required', 'format', 'minlength', 'maxlength', 'min', 'max', 'email', 'codeTaken'];
export function firstMessage(errors: ValidationErrors): string {
for (const key of PRIORITY) {
if (errors[key]) return MESSAGES[key](errors[key]);
}
const [key, value] = Object.entries(errors)[0];
return MESSAGES[key]?.(value) ?? 'Невірне значення'; // запасний варіант
}
<!-- тепер у шаблоні один рядок на поле -->
<input id="code" formControlName="code">
<app-field-error [control]="form.controls.code" [submitted]="submitted()" />
Показувати одразу три повідомлення під полем — це шум. Людина виправляє по одній проблемі за раз, тому й показувати треба найважливішу. Пріоритет очевидний: спершу «заповни», потім «заповни правильно», і аж потім специфічні перевірки на кшталт унікальності.
Звʼязок для скрінрідера
<div class="field">
<label for="code">Код задачі</label>
<input id="code"
formControlName="code"
[attr.aria-invalid]="showError() ? 'true' : null"
[attr.aria-describedby]="showError() ? 'code-error' : null">
@if (showError()) {
<p id="code-error" class="field__error" role="alert">{{ message() }}</p>
}
</div>
Три атрибути роблять помилку доступною:
label for— зв'язує підпис із полем, щоб скрінрідер назвав його.aria-describedby— вказує на текст помилки, і його теж буде озвучено.aria-invalid— повідомляє про стан.
Без цього людина, що користується скрінрідером, чує «поле вводу» й не дізнається, що з ним не так. Детальніше — у модулі 7, але закласти це варто одразу: переробляти потім дорожче.
Як писати повідомлення
- «Формат: KYIV-104» — показує зразок.
- «Мінімум 3 символи» — конкретне число.
- «Такий код уже існує» — пояснює причину.
- «Дата завершення не може бути раніше за початок» — правило словами.
- «Невірне значення» — і що робити?
- «Помилка валідації» — технічна мова.
- «Поле не відповідає шаблону
^[A-Z]{3,6}…» — регулярка користувачу. - «ERROR_CODE_TAKEN» — код замість тексту.
Помилка збереження — не помилка поля
save(): void {
this.saving.set(true);
this.api.create(this.form.getRawValue()).pipe(
finalize(() => this.saving.set(false)),
).subscribe({
next: () => this.dialogRef.close(true),
error: (err: HttpErrorResponse) => {
if (err.status === 409) {
// сервер сказав, що код зайнятий — покажемо це на потрібному полі
this.form.controls.code.setErrors({ codeTaken: true });
this.form.controls.code.markAsTouched();
} else {
this.formError.set('Не вдалось зберегти. Спробуйте ще раз.');
}
},
});
}
Помилку бекенду, яка стосується конкретного поля, треба показувати на цьому полі, а не загальним повідомленням угорі форми. І навпаки: збій мережі не має вішатись на поле — для нього потрібне місце під формою або в шапці модалки.