Angular IDPStrong Junior
Модулі / Форми / Показ помилок
🔥 0

Модуль 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('Не вдалось зберегти. Спробуйте ще раз.');
      }
    },
  });
}

Помилку бекенду, яка стосується конкретного поля, треба показувати на цьому полі, а не загальним повідомленням угорі форми. І навпаки: збій мережі не має вішатись на поле — для нього потрібне місце під формою або в шапці модалки.

≈ 40 хв · +25 XP за урок