Валидация форм

Валидация форм является неотъемлемой частью веб-приложений, позволяющей обеспечить правильность и полноту данных, введённых пользователем. В Angular валидация форм может быть реализована как с помощью реактивных форм (Reactive Forms), так и с использованием шаблонных форм (Template-Driven Forms). Каждый из этих подходов имеет свои особенности и может быть применён в зависимости от требований к проекту.

Шаблонные формы (Template-Driven Forms)

Шаблонные формы — это более простая и декларативная форма, которая подходит для небольших проектов или если необходимо быстро создать интерфейс без сложных бизнес-логик. В этом случае Angular связывает модель данных с представлением через директивы в шаблоне, такие как ngModel, required, minlength и другие.

Пример базовой шаблонной формы с валидацией:

<form #form="ngForm" (ngSubmit)="onSubmit(form)">
  <div>
    <label for="username">Имя пользователя</label>
    <input id="username" name="username" ngModel required minlength="5">
    <div *ngIf="form.controls['username']?.invalid && form.controls['username']?.touched">
      <small *ngIf="form.controls['username']?.errors?.['required']">Поле обязательно для заполнения.</small>
      <small *ngIf="form.controls['username']?.errors?.['minlength']">Имя должно быть не менее 5 символов.</small>
    </div>
  </div>

  <button type="submit" [disabled]="form.invalid">Отправить</button>
</form>

В этом примере форма проверяет поле на обязательность (required) и минимальную длину (minlength). Валидация выполняется на основе встроенных директив, и ошибки отображаются непосредственно в шаблоне.

Реактивные формы (Reactive Forms)

Реактивные формы — это более гибкий и мощный подход, при котором валидация и управление состоянием формы выполняются на уровне компонента. Реактивные формы требуют явного создания и управления формами в классе компонента с использованием FormGroup, FormControl и FormBuilder.

Пример базовой реактивной формы с валидацией:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-registration',
  templateUrl: './registration.component.html',
})
export class RegistrationComponent {
  registrationForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.registrationForm = this.fb.group({
      username: ['', [Validators.required, Validators.minLength(5)]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
    });
  }

  onSubmit(): void {
    if (this.registrationForm.valid) {
      console.log('Форма отправлена', this.registrationForm.value);
    }
  }
}

В шаблоне форма выглядит следующим образом:

<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="username">Имя пользователя</label>
    <input id="username" formControlName="username">
    <div *ngIf="registrationForm.controls['username'].invalid && registrationForm.controls['username'].touched">
      <small *ngIf="registrationForm.controls['username'].errors?.['required']">Поле обязательно для заполнения.</small>
      <small *ngIf="registrationForm.controls['username'].errors?.['minlength']">Имя должно быть не менее 5 символов.</small>
    </div>
  </div>

  <div>
    <label for="email">Электронная почта</label>
    <input id="email" formControlName="email">
    <div *ngIf="registrationForm.controls['email'].invalid && registrationForm.controls['email'].touched">
      <small *ngIf="registrationForm.controls['email'].errors?.['required']">Поле обязательно для заполнения.</small>
      <small *ngIf="registrationForm.controls['email'].errors?.['email']">Неверный формат электронной почты.</small>
    </div>
  </div>

  <div>
    <label for="password">Пароль</label>
    <input id="password" formControlName="password" type="password">
    <div *ngIf="registrationForm.controls['password'].invalid && registrationForm.controls['password'].touched">
      <small *ngIf="registrationForm.controls['password'].errors?.['required']">Поле обязательно для заполнения.</small>
      <small *ngIf="registrationForm.controls['password'].errors?.['minlength']">Пароль должен быть не менее 8 символов.</small>
    </div>
  </div>

  <button type="submit" [disabled]="registrationForm.invalid">Отправить</button>
</form>

Реактивные формы позволяют значительно проще управлять состоянием формы, а также легче работать с динамическими формами и сложными валидациями.

Типы валидаций

В Angular доступны различные типы валидаций:

  • Встроенные валидаторы:

    • required — проверка на обязательность заполнения поля.
    • minlength и maxlength — проверка минимальной и максимальной длины.
    • email — проверка на корректность формата электронной почты.
    • pattern — проверка с использованием регулярных выражений.

    Эти валидаторы могут быть использованы как в шаблонных, так и в реактивных формах.

  • Пользовательские валидаторы: Для создания пользовательских валидаторов необходимо определить функцию, которая будет возвращать объект с ошибками в случае невалидных данных.

Пример пользовательского валидатора:

import { AbstractControl, ValidationErrors } from '@angular/forms';

export function usernameValidator(control: AbstractControl): ValidationErrors | null {
  const forbidden = /admin/i.test(control.value);
  return forbidden ? { forbiddenName: { value: control.value } } : null;
}

Этот валидатор проверяет, не является ли введённое имя пользователя словом “admin” (нечувствительно к регистру).

Асинхронные валидаторы

Асинхронные валидаторы полезны, когда необходимо выполнить проверку данных, требующую обращения к серверу, например, проверка уникальности имени пользователя. В отличие от синхронных валидаторов, асинхронные валидаторы возвращают Observable или Promise.

Пример асинхронного валидатора:

import { AbstractControl, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';

export function usernameAsyncValidator(control: AbstractControl): Observable<ValidationErrors | null> {
  return of(control.value).pipe(
    debounceTime(300),
    switchMap(username => {
      return checkUsernameAvailability(username).pipe(
        map(isAvailable => isAvailable ? null : { usernameTaken: { value: control.value } })
      );
    })
  );
}

function checkUsernameAvailability(username: string): Observable<boolean> {
  // Функция имитирует проверку доступности имени пользователя на сервере.
  return of(username !== 'admin');
}

Этот валидатор проверяет, доступно ли имя пользователя на сервере, и если оно занято, возвращает ошибку usernameTaken.

Управление состоянием формы

Angular позволяет легко отслеживать состояние формы с помощью свойств, таких как valid, invalid, pristine, dirty, touched и untouched.

  • valid / invalid: указывает, прошла ли форма или контрол валидацию.
  • pristine / dirty: отражает, было ли изменено поле.
  • touched / untouched: указывает, было ли поле посещено пользователем.

Пример использования состояния формы:

<div *ngIf="registrationForm.invalid && registrationForm.touched">
  <p>Форма содержит ошибки!</p>
</div>

Валидация на уровне формы и группы

Можно применить валидацию не только к отдельным полям, но и к целым группам или всей форме. Это позволяет реализовывать более сложные проверки, такие как проверка на совпадение пароля и подтверждения пароля.

Пример валидации на уровне группы:

this.registrationForm = this.fb.group({
  password: ['', [Validators.required, Validators.minLength(8)]],
  confirmPassword: ['', [Validators.required]]
}, { validators: this.passwordMatchValidator });

Валидация пароля и подтверждения пароля:

passwordMatchValidator(group: FormGroup): ValidationErrors | null {
  const password = group.get('password')?.value;
  const confirmPassword = group.get('confirmPassword')?.value;
  return password === confirmPassword ? null : { passwordMismatch: true };
}

Поддержка локализации ошибок

Angular позволяет легко локализовать сообщения об ошибках, используя сервисы и механизмы интернационализации (i18n). Для этого можно создать словарь ошибок и привязать его к элементам