1. Отображение ошибок валидации
Формы — это мост между пользователем и вашим приложением. Если пользователь не понимает, почему форма не отправляется, или видит сухое "Ошибка", он скорее всего отправится искать другое приложение. Поэтому важно не только валидировать данные, но и показывать человеку, что именно не так, и как это исправить.
В Angular для этого есть всё необходимое: у каждого поля формы есть статус, ошибки, и удобные способы отобразить их в шаблоне. Начнём с самого простого — Template-driven Forms.
Как узнать, что поле невалидно
Каждое поле, связанное с ngModel, автоматически становится частью формы. Angular создаёт для него объект типа NgModel, который содержит свойства:
- valid / invalid — валидно ли поле
- errors — объект с ошибками валидации (например, { required: true })
- touched — поле было в фокусе и покинуто
- dirty — значение поля менялось
Пример: форма с ошибками
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<label>
Email:
<input
name="email"
ngModel
required
email
#emailCtrl="ngModel"
/>
</label>
<div *ngIf="emailCtrl.invalid && (emailCtrl.dirty || emailCtrl.touched)">
<small *ngIf="emailCtrl.errors?.required" style="color: red;">
Email обязателен
</small>
<small *ngIf="emailCtrl.errors?.email" style="color: red;">
Введите корректный email
</small>
</div>
<label>
Пароль:
<input
name="password"
ngModel
required
minlength="6"
#passCtrl="ngModel"
type="password"
/>
</label>
<div *ngIf="passCtrl.invalid && (passCtrl.dirty || passCtrl.touched)">
<small *ngIf="passCtrl.errors?.required" style="color: red;">
Пароль обязателен
</small>
<small *ngIf="passCtrl.errors?.minlength" style="color: red;">
Минимум {{ passCtrl.errors?.minlength.requiredLength }} символов
</small>
</div>
<button type="submit" [disabled]="userForm.invalid">Зарегистрироваться</button>
</form>
Как это работает?
- Мы задаём локальные переменные #emailCtrl="ngModel" и #passCtrl="ngModel", чтобы обращаться к их статусу.
- Блок с ошибками появляется только если поле невалидно и пользователь уже пытался его заполнить (dirty/touched).
- Для каждой ошибки выводим своё сообщение.
Типичные паттерны
- Всегда показывайте ошибки после того, как пользователь попытался что-то ввести, иначе форма будет пугать его красными ошибками с самого начала.
- Используйте свойства dirty (значение менялось) или touched (потеряно фокус), чтобы контролировать момент появления ошибок.
2. Отображение ошибок: Reactive Forms
Reactive Forms в Angular дают больше контроля и возможностей, но принцип тот же.
Пример формы
В компоненте:
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-register-form',
templateUrl: './register-form.component.html'
})
export class RegisterFormComponent {
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
});
constructor(private fb: FormBuilder) {}
onSubmit() {
if (this.form.valid) {
// Отправляем данные
console.log('Форма отправлена:', this.form.value);
} else {
// Помечаем все поля как touched, чтобы сразу показать ошибки
this.form.markAllAsTouched();
}
}
}
В шаблоне:
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label>
Email:
<input formControlName="email" />
</label>
<div *ngIf="form.get('email')?.invalid && (form.get('email')?.dirty || form.get('email')?.touched)">
<small *ngIf="form.get('email')?.errors?.required" style="color: red;">
Email обязателен
</small>
<small *ngIf="form.get('email')?.errors?.email" style="color: red;">
Некорректный email
</small>
</div>
<label>
Пароль:
<input formControlName="password" type="password" />
</label>
<div *ngIf="form.get('password')?.invalid && (form.get('password')?.dirty || form.get('password')?.touched)">
<small *ngIf="form.get('password')?.errors?.required" style="color: red;">
Пароль обязателен
</small>
<small *ngIf="form.get('password')?.errors?.minlength" style="color: red;">
Минимум {{ form.get('password')?.errors?.minlength.requiredLength }} символов
</small>
</div>
<button type="submit" [disabled]="form.invalid">Зарегистрироваться</button>
</form>
Коротко о механике
- Для каждого поля обращаемся к контролу через form.get('имяПоля').
- Проверяем invalid, а также dirty или touched, чтобы не показывать ошибки сразу.
- Для каждой ошибки выводим своё сообщение.
3. Отправка формы: обработка и UX
Как обрабатывать отправку
- В Template-driven формах используйте (ngSubmit)="onSubmit(form)".
- В Reactive Forms — (ngSubmit)="onSubmit()" (форма доступна через поле класса).
В обработчике:
- Проверяйте валидность формы.
- Если невалидна — помечайте все поля как touched, чтобы ошибки сразу показались.
- Если валидна — отправляйте данные (например, через HTTP-запрос).
Пример для Template-driven:
onSubmit(form: NgForm) {
if (form.valid) {
// Отправляем данные
console.log('Данные формы:', form.value);
} else {
// Пометить все поля как touched
Object.values(form.controls).forEach(control => control.markAsTouched());
}
}
Пример для Reactive Forms (см. выше):
onSubmit() {
if (this.form.valid) {
// Отправляем данные
console.log('Форма отправлена:', this.form.value);
} else {
this.form.markAllAsTouched();
}
}
Блокировка кнопки отправки
Кнопку "Отправить" лучше делать неактивной, если форма невалидна:
<button type="submit" [disabled]="form.invalid">Отправить</button>
Это не отменяет валидацию на стороне клиента и сервера! Никогда не доверяйте только фронтенду — пользователь может подделать форму.
4. Практика: форма регистрации с отображением ошибок
Давайте соберём всё вместе. Пример для Reactive Forms:
register-form.component.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-register-form',
templateUrl: './register-form.component.html'
})
export class RegisterFormComponent {
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
agree: [false, Validators.requiredTrue]
});
submitted = false;
constructor(private fb: FormBuilder) {}
onSubmit() {
this.submitted = true;
if (this.form.valid) {
// Здесь обычно отправляется HTTP-запрос
alert('Регистрация успешна!');
this.form.reset();
this.submitted = false;
} else {
this.form.markAllAsTouched();
}
}
}
register-form.component.html
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label>
Email:
<input formControlName="email" />
</label>
<div *ngIf="form.get('email')?.invalid && (form.get('email')?.dirty || form.get('email')?.touched || submitted)">
<small *ngIf="form.get('email')?.errors?.required" style="color: red;">
Email обязателен
</small>
<small *ngIf="form.get('email')?.errors?.email" style="color: red;">
Некорректный email
</small>
</div>
<label>
Пароль:
<input formControlName="password" type="password" />
</label>
<div *ngIf="form.get('password')?.invalid && (form.get('password')?.dirty || form.get('password')?.touched || submitted)">
<small *ngIf="form.get('password')?.errors?.required" style="color: red;">
Пароль обязателен
</small>
<small *ngIf="form.get('password')?.errors?.minlength" style="color: red;">
Минимум {{ form.get('password')?.errors?.minlength.requiredLength }} символов
</small>
</div>
<label>
<input type="checkbox" formControlName="agree" />
Согласен с правилами
</label>
<div *ngIf="form.get('agree')?.invalid && (form.get('agree')?.dirty || form.get('agree')?.touched || submitted)">
<small style="color: red;">Необходимо согласиться с правилами</small>
</div>
<button type="submit" [disabled]="form.invalid">Зарегистрироваться</button>
</form>
5. Как сделать универсальный компонент для ошибок
В больших формах удобно вынести отображение ошибок в отдельный компонент.
validation-errors.component.ts
import { Component, Input } from '@angular/core';
import { AbstractControl } from '@angular/forms';
@Component({
selector: 'app-validation-errors',
template: `
<ng-container *ngIf="control && control.invalid && (control.dirty || control.touched)">
<small *ngIf="control.errors?.required" style="color: red;">Поле обязательно</small>
<small *ngIf="control.errors?.email" style="color: red;">Некорректный email</small>
<small *ngIf="control.errors?.minlength" style="color: red;">
Минимум {{ control.errors?.minlength.requiredLength }} символов
</small>
<!-- Добавьте другие ошибки по необходимости -->
</ng-container>
`
})
export class ValidationErrorsComponent {
@Input() control!: AbstractControl | null;
}
Использование:
<input formControlName="email" />
<app-validation-errors [control]="form.get('email')"></app-validation-errors>
6. Особенности и нюансы
- Не показывайте ошибки сразу — это раздражает пользователя. Показывайте их после попытки ввода или отправки формы.
- Не забывайте про серверную валидацию. Даже если на клиенте всё идеально, на сервере могут быть свои правила (например, email уже зарегистрирован). Серверные ошибки обычно отображают отдельным сообщением после попытки отправки.
- Если форма большая, используйте markAllAsTouched() — это позволит показать ошибки сразу для всех полей, если пользователь нажал "отправить" с пустой формой.
- В Template-driven формах нет метода markAllAsTouched(), нужно вручную пройтись по всем контролам.
7. Типичные ошибки при отображении ошибок и обработке отправки формы
Ошибка №1: ошибки появляются сразу при открытии формы.
Это происходит, если не проверять dirty или touched у поля. Пользователь пугается красных сообщений ещё до первого ввода. Решение: показывать ошибки только если поле уже изменяли или покидали.
Ошибка №2: обработчик отправки не помечает поля как touched.
Пользователь нажимает "Отправить" — форма не отправляется, но ошибок не видно. Решение: при невалидной форме вызывать markAllAsTouched() (Reactive Forms) или вручную пройтись по контролам (Template-driven).
Ошибка №3: блокировка кнопки "Отправить" только по валидности.
Если просто сделать [disabled]="form.invalid", но не показывать ошибки — пользователь не поймёт, что не так. Решение: всегда показывать причину блокировки.
Ошибка №4: отсутствие серверной валидации.
Пользователь может обойти фронтенд-валидацию, поэтому всегда проверяйте данные и на сервере, и отображайте ошибки от сервера в форме.
Ошибка №5: неочевидные сообщения об ошибках.
"Ошибка в поле" — это неинформативно. Лучше: "Email обязателен", "Пароль слишком короткий" и т.д.
Ошибка №6: забыли сбросить форму после успешной отправки.
Пользователь радуется, но форма осталась заполненной. Решение: после успешной отправки вызывайте form.reset().
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ