1. Отображение ошибок в формах
Пользователь редко заполняет форму идеально с первого раза — и это нормально. Хороший интерфейс всегда "подсказывает", что не так: "Пароль слишком короткий", "Почта невалидна", "Это поле обязательно". Если ошибки не показывать, пользователь будет чувствовать себя как в квесте без подсказок — и, скорее всего, сдастся.
Ошибки в Template-driven формах
В шаблонных формах Angular автоматически отслеживает состояние каждого поля: его валидность, был ли к нему доступ (touched, dirty), и какие ошибки возникли. Всё это доступно через локальную переменную, которую мы объявляем прямо в шаблоне с помощью директивы #:
<input name="email" ngModel required email #emailCtrl="ngModel">
<div *ngIf="emailCtrl.invalid && (emailCtrl.dirty || emailCtrl.touched)">
<small *ngIf="emailCtrl.errors?.required">Поле обязательно!</small>
<small *ngIf="emailCtrl.errors?.email">Введите корректный email!</small>
</div>
Пояснения:
emailCtrl.invalid — поле невалидно.
emailCtrl.dirty — пользователь что-то менял.
emailCtrl.touched — пользователь хотя бы раз зашёл на поле.
Обычно ошибки показывают только если пользователь уже попытался что-то ввести или ушёл с поля, чтобы не пугать его "красным" до первого клика.
Пример: форма регистрации
<form #regForm="ngForm" (ngSubmit)="onSubmit(regForm)">
<input name="username" ngModel required minlength="3" #userCtrl="ngModel">
<div *ngIf="userCtrl.invalid && (userCtrl.dirty || userCtrl.touched)">
<small *ngIf="userCtrl.errors?.required">Имя обязательно</small>
<small *ngIf="userCtrl.errors?.minlength">Минимум 3 символа</small>
</div>
<button type="submit" [disabled]="regForm.invalid">Зарегистрироваться</button>
</form>
Ошибки в Reactive формах
В реактивных формах всё ещё интереснее: все ошибки и состояния доступны через FormControl (или FormGroup). Обычно мы описываем форму в компоненте:
import { FormBuilder, Validators } from '@angular/forms';
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
В шаблоне обращаемся к контролям через геттеры или напрямую:
<input formControlName="email">
<div *ngIf="form.get('email')?.invalid && (form.get('email')?.dirty || form.get('email')?.touched)">
<small *ngIf="form.get('email')?.errors?.required">Email обязателен</small>
<small *ngIf="form.get('email')?.errors?.email">Некорректный email</small>
</div>
Частая практика: для удобства делаем геттеры в компоненте:
get email() { return this.form.get('email'); }
И тогда в шаблоне:
<input formControlName="email">
<div *ngIf="email?.invalid && (email?.dirty || email?.touched)">
<small *ngIf="email?.errors?.required">Email обязателен</small>
<small *ngIf="email?.errors?.email">Некорректный email</small>
</div>
Пример: форма авторизации
form = this.fb.group({
login: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(6)]]
});
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="login" placeholder="Логин">
<div *ngIf="form.get('login')?.invalid && (form.get('login')?.dirty || form.get('login')?.touched)">
<small *ngIf="form.get('login')?.errors?.required">Введите логин</small>
</div>
<input type="password" formControlName="password" placeholder="Пароль">
<div *ngIf="form.get('password')?.invalid && (form.get('password')?.dirty || form.get('password')?.touched)">
<small *ngIf="form.get('password')?.errors?.required">Введите пароль</small>
<small *ngIf="form.get('password')?.errors?.minlength">Минимум 6 символов</small>
</div>
<button type="submit" [disabled]="form.invalid">Войти</button>
</form>
2. Обработка отправки формы
Template-driven формы
В шаблонных формах обработчик отправки обычно выглядит так:
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)">
<!-- ... -->
<button type="submit">Отправить</button>
</form>
onSubmit(form: NgForm) {
if (form.valid) {
console.log('Значения формы:', form.value);
// Тут можно отправить данные на сервер
} else {
// Можно подсветить ошибки
console.log('Форма невалидна');
}
}
Пояснение:
form.value — объект с данными всех полей.
form.valid — булево, форма валидна ли.
Reactive формы
В реактивных формах всё аналогично, но мы работаем с экземпляром FormGroup:
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- ... -->
<button type="submit">Отправить</button>
</form>
onSubmit() {
if (this.form.valid) {
console.log('Отправляем:', this.form.value);
// Отправка данных
} else {
// Можно выделить все поля как touched, чтобы показать ошибки
this.form.markAllAsTouched();
}
}
Фишка: markAllAsTouched() — подсвечивает все ошибки, даже если пользователь не заходил в поля.
3. Получение значений из формы
Template-driven
В шаблонных формах значения доступны через объект формы:
onSubmit(form: NgForm) {
// form.value — это объект вида { email: '...', password: '...' }
const { email, password } = form.value;
// Используем значения
}
Reactive
В реактивных формах всё аналогично:
onSubmit() {
const { email, password } = this.form.value;
// Используем значения
}
Для частого доступа к отдельному полю:
const emailValue = this.form.get('email')?.value;
Получение "сырых" и "валидных" значений
В реактивных формах есть методы:
form.getRawValue() — возвращает значения всех контролов, даже если они disabled.
form.value — возвращает только enabled-поля.
4. Массовое отображение ошибок: универсальный подход
Чтобы не писать кучу одинакового кода для каждого поля, можно сделать универсальный компонент или функцию для вывода ошибок.
Пример: компонент для ошибок
// error-messages.component.ts
import { Input } from '@angular/core';
import { AbstractControl } from '@angular/forms';
@Component({
selector: 'app-error-messages',
template: `
<div *ngIf="control?.invalid && (control?.dirty || control?.touched)">
<small *ngIf="control?.errors?.required">Обязательное поле</small>
<small *ngIf="control?.errors?.email">Некорректный email</small>
<small *ngIf="control?.errors?.minlength">Минимум {{control?.errors?.minlength.requiredLength}} символов</small>
<!-- Добавьте другие ошибки по необходимости -->
</div>
`
})
export class ErrorMessagesComponent {
@Input() control?: AbstractControl | null;
}
Использование:
<input formControlName="email">
<app-error-messages [control]="form.get('email')"></app-error-messages>
5. Сравнение Template-driven и Reactive Forms
| Характеристика | Template-driven (шаблонные) | Reactive (реактивные) |
|---|---|---|
| Где описывается логика | В шаблоне (HTML) | В компоненте (TypeScript) |
| Связывание | Директивы ngModel, ngForm | formControl, formGroup, formArray |
| Масштабируемость | Хорошо для простых, маленьких форм | Идеально для сложных, больших форм |
| Валидация | В основном через атрибуты/директивы | Через валидаторы, можно кастомные |
| Тестируемость | Сложнее тестировать | Легко тестировать |
| Динамическое создание | Сложно реализовать | Очень просто |
| Производительность | Чуть медленнее на больших формах | Лучше масштабируется |
| Типичные сценарии | Регистрация, логин, простые формы | Большие анкеты, динамические формы |
Когда выбирать какой подход?
Template-driven: если у вас простая форма, мало логики, не нужно динамически добавлять поля, и вы не любите много TypeScript-кода.
Reactive: если форма большая, сложная, много условий, динамические поля, сложная валидация, интеграция с внешними сервисами, или вы хотите писать тесты.
Могут ли формы сочетаться?
Да, Angular позволяет смешивать оба подхода, но лучше придерживаться одного стиля в рамках одного компонента — так проще поддерживать код и не путаться.
6. Типичные ошибки при работе с формами
Ошибка №1: Не подключён FormsModule или ReactiveFormsModule.
Если забыть импортировать нужный модуль в app.module.ts, Angular не поймёт директивы (ngModel, formGroup и т.д.) и выдаст ошибку типа "Can't bind to 'formGroup' since it isn't a known property".
Ошибка №2: Не указан name в input для Template-driven.
Без атрибута name Angular не сможет отслеживать поле в шаблонной форме. Всё работает, только если у каждого поля есть уникальное имя!
Ошибка №3: Не подписаны на изменения формы.
В реактивных формах часто забывают подписаться на изменения (valueChanges, statusChanges), если нужно реагировать на ввод пользователя.
Ошибка №4: Показываются ошибки до первого взаимодействия.
Не стоит показывать сообщения об ошибках сразу после рендера формы — это отпугивает пользователя. Лучше показывать их только после того, как поле стало dirty или touched.
Ошибка №5: Не обновляются ошибки при динамическом изменении валидаторов.
Если вы динамически добавляете/убираете валидаторы в реактивной форме, не забудьте вызвать updateValueAndValidity() для контролов.
Ошибка №6: Смешивание Template-driven и Reactive подходов в одном компоненте.
Это приводит к путанице и неочевидным багам. Лучше придерживаться одного стиля на компонент.
Ошибка №7: Не используете markAllAsTouched() при отправке.
Если пользователь отправляет пустую форму, а вы не вызвали markAllAsTouched(), ошибки могут не появиться — пользователь не поймёт, что делать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ