一个没有验证的表单,就像一座不设防的城池,任何人、任何「数据」都可以长驱直入。其结果,轻则让你的应用状态陷入混乱,重则导致脏数据入库,引发更深层次的灾难。

表单验证,就是你应用数据的「护城河」与「城墙」。它是你作为一名严谨的工程师,为保障数据质量、提升用户体验而必须掌握的「秘密武器」。幸运的是,Angular 为我们提供了 arsenal(一整套)强大而灵活的验证「兵器谱」。

两大阵营:不同的「练兵」方式#

在 Angular 中,应用验证规则的方式,根据你选择的表单类型而有所不同:

  • 响应式表单:验证器是函数。你在组件的 TS 文件中,将这些函数直接传递给 FormControl 的构造器。逻辑与模型在代码中紧密结合。

  • 模板驱动表单:验证器是指令。你将 required, minLength 等指令直接应用在模板的 HTML 元素上。逻辑体现在视图中。

虽然应用方式不同,但验证器本身的核心(无论是内置的还是自定义的)是可以通用的。

「兵器谱」之:内置验证器#

@angular/forms 中的 Validators 类,为我们提供了最常用的一批「制式兵器」,开箱即用。

  • Validators.required:必填项。

  • Validators.minLength(number):最小长度。

  • Validators.maxLength(number):最大长度。

  • Validators.pattern(regex):必须匹配正则表达式。

  • Validators.email:必须是合法的邮件格式。

  • Validators.min(number):最小值(用于数字)。

  • Validators.max(number):最大值(用于数字)。

如何使用?

响应式表单中:

import { Validators } from '@angular/forms';
this.form = this.fb.group({
  // 将校验器函数数组作为第二个参数传入
  username: ['', [Validators.required, Validators.minLength(3)]],
  email: ['', [Validators.required, Validators.email]]
});

模板驱动表单中:

<input type="text" name="username" ngModel required minlength="3">
<input type="email" name="email" ngModel required email>

「锻造神兵」之一:自定义同步验证器#

当内置兵器不称手时,我们就需要自己「锻造」一柄。比如,我们需要一个不允许输入特殊字符的验证器。

规则:一个同步验证器,就是一个接收 AbstractControl 作为参数,并在验证失败时返回一个错误对象 ValidationErrors验证成功时返回 null 的函数。

// no-special-chars.validator.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function noSpecialCharsValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const hasSpecialChars = /[!@#$%^&*(),.?":{}|<>]/.test(control.value);
    // 如果包含特殊字符,返回一个错误对象,键名代表错误类型
    return hasSpecialChars ? { hasSpecialChars: true } : null;
  };
}

在响应式表单中使用:

this.form = this.fb.group({
  username: ['', [Validators.required, noSpecialCharsValidator()]]
});

(在模板驱动表单中使用自定义验证器,需要将其包装成一个指令,相对繁琐,故在此不赘述,推荐在需要自定义验证时优先使用响应式表单。)

「锻造神兵」之二:自定义异步验证器#

当验证逻辑需要依赖外部系统时(如检查用户名是否已被注册),异步验证器就登场了。

规则:一个异步验证器,是一个接收 AbstractControl,并返回一个 Promise<ValidationErrors | null>Observable<ValidationErrors | null> 的函数。

实战:检查用户名是否唯一

// unique-username.validator.ts
import { inject, Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { AuthService } from './auth.service';
// 我们通常将异步验证器封装在一个服务中,以便注入其他依赖
@Injectable({ providedIn: 'root' })
export class UniqueUsernameValidator {
  private authService = inject(AuthService);
  createValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      return of(control.value).pipe(
        debounceTime(500), // 防抖,避免频繁请求
        distinctUntilChanged(), // 值未变则不请求
        switchMap(username => 
          this.authService.isUsernameTaken(username).pipe(
            map(isTaken => (isTaken ? { usernameTaken: true } : null)),
            catchError(() => of(null)) // 如果 API 请求失败,也视作验证通过
          )
        )
      );
    };
  }
}

在响应式表单中使用:

异步验证器是 FormControl第三个参数。

// component.ts
constructor(private uniqueValidator: UniqueUsernameValidator) {
  this.form = this.fb.group({
    username: [
      '', // 默认值
      [Validators.required], // 同步验证器
      [this.uniqueValidator.createValidator()] // 异步验证器
    ]
  });
}

当异步验证正在「请求」时,FormControl 的状态会变为 PENDING,你可以利用这个状态来显示「正在验证…」之类的提示。

展示「战报」:在模板中显示错误信息#

知道了如何验证,我们还需要优雅地把「战报」(错误信息)展示给用户。

<form [formGroup]="form">
  <input formControlName="username">
  @if (form.get('username'); as usernameControl) {
    @if (usernameControl.invalid && (usernameControl.dirty || usernameControl.touched)) {
      <div class="error-feedback">
        @if (usernameControl.hasError('required')) {
          <span>用户名不能为空。</span>
        }
        @if (usernameControl.hasError('minlength')) {
          <span>用户名至少需要3个字符。</span>
        }
        @if (usernameControl.hasError('usernameTaken')) {
          <span>该用户名已被占用。</span>
        }
      </div>
    } @else if (usernameControl.pending) {
      <div class="pending-feedback">正在检查用户名可用性...</div>
    }
  }
</form>

我们通过检查控件的 invalid, dirty, touched, pending 等状态,以及 hasError('errorKey') 方法,来精准地控制错误信息的显示时机和内容。

结语#

表单验证,是应用程序的「免疫系统」。它能将无效的、有害的「数据病菌」阻挡在外,保障内部系统的健康与纯净。Angular 提供了从「常规武器」到「可定制神兵」的全套工具,让你能从容应对各种复杂的校验场景。

掌握好这套「秘密武器」,你就能为你的应用,构建起一道真正「滴水不漏」的坚固防线。

正如《易经》有云:「君子以思患而豫防之。」意指君子深思可能发生的祸患,从而提前预防。表单验证正是这种「思患豫防」的智慧在编程世界中的体现,它是我们保障数据质量、维护系统稳定的基石。