Angular 表单验证完全指南:模板驱动与响应式表单的校验体系与自定义验证器实战
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
在 Angular 中,表单验证是保障数据质量与用户体验的核心能力。无论你使用模板驱动表单还是响应式表单,Angular 都提供了一套"属性/函数声明校验规则 → 值变化自动触发 → 状态对象存储结果 → 模板按状态渲染提示"的完整闭环。本文以 angular 仓库中adev/src/content/examples/form-validation的"演员表单(Actor Form)"示例为主线,系统讲解内置验证器、自定义同步验证器、跨字段验证、异步验证器(如校验角色是否已被占用)以及验证器的运行时动态管理,并深入到packages/forms的源码实现,帮助你掌握既能防错又能优雅提示错误的完整方案。
模板驱动表单中的输入验证
模板驱动表单的验证方式与原生 HTML 表单验证一脉相承:你只需在元素上添加与 HTML 原生约束验证一致的验证属性(如required、minlength),Angular 便通过指令把这些属性与框架中的验证器函数一一对应起来(见 validators.ts 中RequiredValidator、MinLengthValidator等指令的注册方式)。
每次表单控件的值发生变化时,Angular 都会执行验证,并产生两类结果之一:
- 一组验证错误(
ValidationErrors对象),对应控件的INVALID状态; null,对应控件的VALID状态。
在模板中,你可以通过把ngModel导出到局部模板变量来检查控件状态。下面这个示例把NgModel导出为名为name的变量(源码见 actor-form-template.component.html):
<input type="text" id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel" /> @if (name.invalid && (name.dirty || name.touched)) { <div class="alert"> @if (name.hasError('required')) { <div>Name is required.</div> } @if (name.hasError('minlength')) { <div>Name must be at least 4 characters long.</div> } @if (name.hasError('forbiddenName')) { <div>Name cannot be Bob.</div> } </div> }上述示例展示了几个关键特性:
<input>元素携带了 HTML 验证属性required与minlength,同时携带了一个自定义验证指令appForbiddenName(对应forbiddenName验证器)。#name="ngModel"把NgModel导出为局部变量name。NgModel镜像了其底层FormControl实例的许多属性,因此可在模板中使用它检查诸如valid、dirty之类的控件状态。控件属性的完整列表可参见 AbstractControl 定义。- 最外层的
@if仅在name无效且控件处于dirty或touched状态时,才显示一组嵌套的错误消息。 - 内层每个
@if针对一种可能的验证错误展示自定义消息:required、minlength与forbiddenName各有一条。
重要实践提示:为避免验证器在用户有机会编辑表单之前就显示错误,应始终在控件上检查dirty或touched状态。Angular 的状态流转规则是:
- 当用户修改了所关注字段的值时,控件被标记为dirty(脏);
- 当用户让表单控件失去焦点(blur)时,控件被标记为touched(已触碰)。
在模板驱动表单的组件类中,只需在imports中引入对应的验证指令即可激活上述机制(见 actor-form-template.component.ts):它导入了FormsModule、ForbiddenValidatorDirective、UnambiguousRoleValidatorDirective与UniqueRoleValidatorDirective。
响应式表单中的输入验证
在响应式表单中,唯一事实来源(source of truth)是组件类。你不再通过模板属性添加验证器,而是直接在组件类的表单控件模型上添加验证器函数,Angular 会在控件值变化时调用这些函数(响应式表单整体机制可参见ReactiveFormsModule及 validators.ts)。
验证器函数的两种类型
| 验证器类型 | 细节 |
|---|---|
| 同步验证器(Sync validators) | 同步函数,接收控件实例后立即返回一组验证错误或null。在实例化FormControl时作为第二个参数传入。 |
| 异步验证器(Async validators) | 异步函数,接收控件实例后返回一个 Promise 或 Observable,稍后发出(emit)一组验证错误或null。在实例化FormControl时作为第三个参数传入。 |
出于性能考虑,Angular只有全部同步验证器通过后才会运行异步验证器;并且每个异步验证器都必须完成,之后才会设置错误状态。
内置验证器函数
你可以编写自己的验证器函数,也可以直接使用 Angular 内置的验证器。模板驱动表单中以属性形式出现的内置验证器(如required、minlength),全部能以函数形式从Validators类中取得(内置验证器集合定义于 validators.ts)。
把"演员表单"改造成响应式表单后,同样的内置验证器这次以函数形式出现(见 actor-form-reactive.component.1.ts):
actorForm = new FormGroup({ name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), // <-- 传入自定义验证器的方式 ]), role: new FormControl(this.actor.role), skill: new FormControl(this.actor.skill, Validators.required), }); get name() { return this.actorForm.get('name'); } get skill() { return this.actorForm.get('skill'); }此例中name控件设置了两个内置验证器——Validators.required与Validators.minLength(4)——以及一个自定义验证器forbiddenNameValidator。它们都是同步验证器,因此作为第二个参数传入;注意把多个验证函数放进一个数组即可同时支持多个验证器。
该示例还添加了几个 getter 方法。在响应式表单中,你随时可以通过父级 group 的get方法访问任意表单控件,但有时在组件类里定义 getter 作为模板的"快捷方式"会更为方便。再看name输入的模板(见 actor-form-reactive.component.html):
<input type="text" id="name" class="form-control" formControlName="name" required /> @if (name.invalid && (name.dirty || name.touched)) { <div class="alert alert-danger"> @if (name.hasError('required')) { <div>Name is required.</div> } @if (name.hasError('minlength')) { <div>Name must be at least 4 characters long.</div> } @if (name.hasError('forbiddenName')) { <div>Name cannot be Bob.</div> } </div> }这个表单与模板驱动版本的区别在于:它不再导出任何指令,而是使用组件类中定义的namegetter。请注意required属性仍然保留在模板中——它虽然对验证不是必需的,但为了**可访问性(accessibility)**应当保留。
定义自定义验证器
内置验证器并不总能精确匹配你的业务场景,因此有时需要创建自定义验证器。以forbiddenNameValidator函数为例(见 forbidden-name.directive.ts):
/** An actor's name can't match the given regular expression */ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value); return forbidden ? {forbiddenName: {value: control.value}} : null; }; }这个函数是一个工厂(factory):它接收一个用于检测某种"禁止名称"的正则表达式,并返回一个验证器函数。在本示例中禁止名是 "bob",因此该验证器会拒绝任何包含 "bob" 的演员名字;换一个场景,它也可以拒绝 "alice" 或其他被配置正则匹配到的任何名字。
forbiddenNameValidator工厂返回配置好的验证器函数,该函数接收一个 Angular 控件对象,并返回:
null——当控件值有效时;- 一个验证错误对象——否则。
错误对象通常带有一个属性,其属性名即验证键'forbiddenName',其值则是任意字典,可把值(例如这里的{value})插入到错误消息中。
自定义异步验证器与同步版本相似,但必须改为返回一个稍后发出null或验证错误对象的 Promise 或 observable。对于 observable 情形,该 observable必须完成(complete),此时表单会采用其最后发出的值作为验证结果。
向响应式表单添加自定义验证器
在响应式表单中,只需把函数直接传给FormControl即可(见 actor-form-reactive.component.1.ts):
name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), // <-- 把自定义验证器以函数形式传入数组 ]),向模板驱动表单添加自定义验证器
在模板驱动表单中,需要在模板里添加一个指令,由指令包装验证器函数。对应的ForbiddenValidatorDirective就是forbiddenNameValidator的包装器。Angular 之所以能识别该指令在验证流程中的角色,是因为指令通过NG_VALIDATORS提供者(provider)注册了自己——NG_VALIDATORS是一个预定义、可扩展的验证器集合:
@Directive({ selector: '[appForbiddenName]', providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => ForbiddenValidatorDirective), multi: true, }, ], }) export class ForbiddenValidatorDirective implements Validator { readonly forbiddenName = input<string>('', {alias: 'appForbiddenName'}); validate(control: AbstractControl): ValidationErrors | null { return this.forbiddenName() ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control) : null; } }指令类实现了Validator接口,从而能便捷地与 Angular 表单集成(该文件同时展示了模板驱动版本如何用新式的input()信号读取属性值,再按需把forbiddenName编译为正则并调用工厂函数)。NG_VALIDATORS的注册与扩展机制定义在 packages/forms/src/directives/validators.ts。
指令就绪后,只需把它的选择器appForbiddenName添加到任意输入元素上即可激活它:
<input ... appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel" />提示(关键陷阱):注意自定义验证指令是用useExisting而非useClass实例化的。注册的验证器必须是ForbiddenValidatorDirective 的这个实例——即表单里forbiddenName属性被绑定为 "bob" 的那个实例。若把useExisting换成useClass,你注册的是一个全新的类实例,它并没有forbiddenName值,验证逻辑也就无法按预期工作。
控件状态 CSS 类
Angular 会自动把许多控件状态镜像为表单控件元素上的 CSS 类,你可以利用这些类按表单状态为控件设置样式。目前支持以下类:
.ng-valid.ng-invalid.ng-pending.ng-pristine.ng-dirty.ng-untouched.ng-touched.ng-submitted(仅作用于外层<form>元素)
在演员表单示例中,使用.ng-valid与.ng-invalid类为每个表单控件的边框着色(见 forms.css):
.ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } .ng-invalid:not(form) { border-left: 5px solid #a94442; /* red */ }这样一来,通过(.ng-valid)的字段显示绿色左边框,未通过的字段(.ng-invalid)显示红色左边框,用户无需任何提示文字即可一眼看出问题字段。注意示例中还特别排除了form元素本身,避免把红色边框渲染到整个表单容器上。
跨字段验证(Cross-field Validation)
跨字段验证器是一种自定义验证器,它比较表单中不同字段的值,并决定是否把它们组合起来接受或拒绝。典型场景包括:表单提供互斥选项(用户可选 A 或 B,但不能同时选);或某个字段依赖另一个字段(只有选了 A 才允许选 B)。
下面的跨字段验证示例演示了如何做到两点:
- 基于两个兄弟控件(sibling controls)的值,校验响应式或模板驱动表单的输入;
- 在用户与表单交互之后且验证失败时,显示一条描述性错误消息。
示例业务规则:演员的名字不能与所扮演的角色名相同(否则角色产生歧义),以此确保演员没有在自己的角色里复用名字。验证器通过检查演员的 name 与 role 是否一致来实现。
响应式表单的跨字段验证
表单结构如下:
const actorForm = new FormGroup({ 'name': new FormControl(), 'role': new FormControl(), 'skill': new FormControl(), });注意name和role是兄弟控件。要在一个自定义验证器里同时评估这两个控件,就必须在它们的共同祖先控件——FormGroup——上执行验证,然后通过查询FormGroup取得子控件以比较它们的值。给FormGroup添加验证器的方式,是在创建时作为第二个参数传入:
const actorForm = new FormGroup( { 'name': new FormControl(), 'role': new FormControl(), 'skill': new FormControl(), }, {validators: unambiguousRoleValidator}, );验证器代码如下(见 unambiguous-role.directive.ts):
/** An actor's name can't match the actor's role */ export const unambiguousRoleValidator: ValidatorFn = ( control: AbstractControl, ): ValidationErrors | null => { const name = control.get('name'); const role = control.get('role'); return name && role && name.value === role.value ? {unambiguousRole: true} : null; };unambiguousRoleValidator实现了ValidatorFn接口:接收一个 Angular 控件对象作为参数,若表单有效返回null,否则返回ValidationErrors。验证器通过调用FormGroup的get方法取得子控件,再比较name与role的值:
- 值不相同→ 角色无歧义,两个控件都有效,返回
null; - 值相同→ 演员的角色有歧义,返回错误对象将表单标记为无效。
为了更好的用户体验,模板会在表单无效时显示合适的错误消息(见 actor-form-reactive.component.html):
@if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) { <div class="cross-validation-error-message alert alert-danger"> Name cannot match role or audiences will be confused. </div> }这个@if仅在FormGroup携带unambiguousRoleValidator返回的跨字段验证错误、且用户已经与该表单完成交互(touched或dirty)时才显示错误。
模板驱动表单的跨字段验证
对模板驱动表单,需要创建一个指令来包装验证器函数,并通过NG_VALIDATORS令牌把该指令作为验证器提供出去:
@Directive({ selector: '[appUnambiguousRole]', providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => UnambiguousRoleValidatorDirective), multi: true, }, ], }) export class UnambiguousRoleValidatorDirective implements Validator { validate(control: AbstractControl): ValidationErrors | null { return unambiguousRoleValidator(control); } }然后把新指令添加到 HTML 模板中。由于验证器必须注册在表单的最高层级,下列模板把指令放在<form>标签上:
<form #actorForm="ngForm" appUnambiguousRole> ... </form>当表单无效时显示错误消息(见 actor-form-template.component.html):
@if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) { <div class="cross-validation-error-message alert">Name cannot match role.</div> }这条提示逻辑在模板驱动与响应式表单中是完全相同的。
创建异步验证器
异步验证器实现AsyncValidatorFn与AsyncValidator接口。它们与同步版本非常相似,区别在于:
validate()函数必须返回 Promise 或 observable;- 返回的 observable 必须是**有限(finite)**的,即必须在某个时刻完成。要把无限 observable 转换为有限版本,可通过
first、last、take、takeUntil之类的过滤操作符进行 pipe。
异步验证发生在同步验证之后,且只有同步验证成功时才会执行。这一检查让表单在更基础的验证已判定输入无效时,可以避免触发代价高昂的异步验证流程(例如 HTTP 请求)。
异步验证开始后,表单控件进入pending(待定)状态。可检查控件的pending属性,并据此为持续中的验证操作提供视觉反馈。一种常见 UI 模式是在异步验证进行时显示 spinner,如下所示(模板驱动表单):
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator /> @if (model.pending) { <app-spinner /> }实现自定义异步验证器
在下面这个示例中,异步验证器保证演员不会接到一个已被占用的角色。新演员不断试镜、老演员不断退休,因此可用角色列表无法提前获取;要校验潜在的角色输入,验证器必须发起一次异步操作,去查询当前所有已确定演员的中央数据库。下面创建了实现AsyncValidator接口的验证器类UniqueRoleValidator(见 role.directive.ts):
@Injectable({providedIn: 'root'}) export class UniqueRoleValidator implements AsyncValidator { private readonly actorsService = inject(ActorsService); validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.actorsService.isRoleTaken(control.value).pipe( map((isTaken) => (isTaken ? {uniqueRole: true} : null)), catchError(() => of(null)), ); } }actorsService属性以ActorsService令牌的实例初始化,其接口定义为:
interface ActorsService { isRoleTaken: (role: string) => Observable<boolean>; }在真实应用中,ActorsService负责向演员数据库发起 HTTP 请求来检查角色是否可用。从验证器的角度看,服务的具体实现并不重要,因此示例可以直接面向ActorsService接口编码。配套的 actors.service.ts 提供了一个带 400ms 延迟的模拟实现,内置ROLES = ['Hamlet', 'Ophelia', 'Romeo', 'Juliet']供演示。
验证开始后,UniqueRoleValidator用当前控件值委托调用ActorsService.isRoleTaken()。此时控件被标记为pending,并一直保持该状态,直到validate()方法返回的 observable 链完成。isRoleTaken()分发 HTTP 请求检查角色是否可用并返回Observable<boolean>;validate()通过map操作符把响应转换为验证结果,然后和任何验证器一样:有效返回null,无效返回ValidationErrors。
该验证器还用catchError操作符处理潜在错误——这里把isRoleTaken()的失败视为验证通过,因为验证请求失败并不代表角色一定无效;你当然也可以改用不同的错误处理方式,直接返回ValidationError对象。一段时间后 observable 链完成、异步验证结束,pending标志被置为false,表单的有效性随之更新。
向响应式表单添加异步验证器
首先把验证器注入为组件类的一个属性(见 actor-form-reactive.component.2.ts):
roleValidator = inject(UniqueRoleValidator);然后把验证器函数直接传给FormControl以应用它。下面的示例把UniqueRoleValidator的validate函数应用到roleControl上:通过asyncValidators选项传入,并绑定到注入进ActorFormReactiveComponent的那个UniqueRoleValidator实例:
ngOnInit(): void { const roleControl = new FormControl('', { asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)], updateOn: 'blur', }); roleControl.setValue(this.actor.role); this.actorForm = new FormGroup({ name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), ]), role: roleControl, skill: new FormControl(this.actor.skill, Validators.required), }); }asyncValidators的值可以是单个异步验证器函数,也可以是函数数组。关于FormControl选项的更多细节可参考AbstractControlOptionsAPI。
向模板驱动表单添加异步验证器
模板驱动表单中需要创建一个新指令,并在其上注册NG_ASYNC_VALIDATORS提供者。下例中指令注入含有真实验证逻辑的UniqueRoleValidator类,并在 Angular 触发验证时于validate函数内调用它(见 role.directive.ts):
@Directive({ selector: '[appUniqueRole]', providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => UniqueRoleValidatorDirective), multi: true, }, ], }) export class UniqueRoleValidatorDirective implements AsyncValidator { private readonly validator = inject(UniqueRoleValidator); validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.validator.validate(control); } }然后与同步验证器一样,把指令的选择器加到输入元素上即可激活它(见 actor-form-template.component.html 中的 role 输入区):
<input type="text" id="role" name="role" #role="ngModel" [(ngModel)]="actor.role" [ngModelOptions]="{updateOn: 'blur'}" appUniqueRole /> @if (role.pending) { <div>Validating...</div> } @if (role.invalid) { <div class="alert role-errors"> @if (role.hasError('uniqueRole')) { <div>Role is already taken.</div> } </div> }该模板片段同时示范了模板驱动表单中对异步验证的两个关键反馈:通过role.pending显示"正在校验",以及通过role.hasError('uniqueRole')展示"该角色已被占用"。
异步验证器的性能优化
默认情况下,每次表单值变化后所有验证器都会运行。同步验证器通常不会对应用性能造成可感知的影响;而异步验证器通常会发起某种 HTTP 请求来校验控件——每次按键都触发一次 HTTP 请求会给后端 API 带来压力,应尽量避免。
可以通过把updateOn属性从默认的change改为submit或blur来推迟更新表单有效性:
- 模板驱动表单:在模板中设置该属性:
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}" />- 响应式表单:在
FormControl实例中设置:
new FormControl('', {updateOn: 'blur'});在 actors 示例中,响应式与模板驱动两个版本都把角色字段的更新时机设为blur,从而避免每次按键都触发isRoleTaken的异步查询(模拟服务仍自带 400ms 延迟以便肉眼观察 pending 状态)。
在响应式表单中动态管理验证器
在复杂响应式表单中,你可能需要根据用户输入或应用状态新增、移除或修改验证器。AbstractControl(定义于 packages/forms/src/model/abstract_model.ts)为此提供了多个方法,无需重建表单控件即可在运行时管理验证器。
添加与移除验证器
addValidators与removeValidators方法允许你在初始化之后修改控件的验证器集合。例如:根据用户选择的国家,动态决定邮编字段是否必填、是否需匹配美国邮编格式:
onCountryChange(country: string) { const postalCodeControl = this.profileForm.get('postalCode'); if (country === 'US') { // 为美国邮编添加验证器 postalCodeControl.addValidators([Validators.required, Validators.pattern(/^\d{5}$/)]); } else { // 非美国时移除验证器 postalCodeControl.removeValidators([Validators.required]); } postalCodeControl.updateValueAndValidity(); }整体替换或清空验证器
使用setValidators替换控件上的全部现有同步验证器,或用clearValidators移除所有验证器。例如根据一个"严格模式"开关切换姓名字段的校验强度:
toggleStrictNameValidation(isStrict: boolean) { const nameControl = this.profileForm.get('name'); if (enable) { // 设置严格验证规则 nameControl.setValidators([ Validators.required, Validators.minLength(3), Validators.pattern(/^[a-zA-Z]+$/), ]); } else { // 清空所有验证器 nameControl.clearValidators(); } nameControl.updateValueAndValidity(); }同样的模式也适用于异步验证器,对应方法为addAsyncValidators、removeAsyncValidators、setAsyncValidators与clearAsyncValidators。
触发验证更新
修改验证器之后,应调用updateValueAndValidity重新计算控件的验证状态。该方法接收选项参数以控制更新行为:
// 更新控件并通知父级 control.updateValueAndValidity(); // 只更新控件自身,不通知父级、不发出事件 control.updateValueAndValidity({onlySelf: true, emitEvent: false});从源码(abstract_model.ts 中updateValueAndValidity的实现)可以看到,它除了执行校验器外,还会负责把新的状态同步到_status字段、更新_updateTreeValidity链并(默认)向父级传播,因此任何对验证器集合的运行时改动都需要这一调用来让状态树保持一致。
与原生 HTML 表单验证的协同
默认情况下,Angular 通过给外层<form>添加novalidate属性来禁用原生 HTML 表单验证(浏览器的约束验证 UI 不会弹出),并改用指令把这些属性与框架内的验证器函数匹配起来。如果你希望配合Angular 验证一起使用原生验证,可以通过ngNativeValidate指令重新启用它。此时两种机制会叠加:例如先借助浏览器的原生约束(如type="email"、pattern)拦截明显非法输入,再让 Angular 的验证体系接管状态展示与自定义业务规则。
小结
Angular 的表单验证体系可以用一条主线概括:声明式规则 + 状态驱动 UI。模板驱动表单用 HTML 属性加NG_VALIDATORS/NG_ASYNC_VALIDATORS指令注册规则,响应式表单用Validators函数与FormControl构造参数声明规则;控件值一旦变化,同步验证先行、异步验证随后,错误对象写入控件的errors,状态则反映在VALID/INVALID/PENDING与dirty/touched等标志及自动生成的 CSS 类上。跨字段验证把校验提升到FormGroup层级处理字段间约束,异步验证器配合updateOn优化网络开销,而addValidators/setValidators等方法则为复杂业务场景提供了运行时的动态验证能力。掌握这一套体系后,无论表单多复杂,你都能用统一、可维护的方式保证输入质量并给出恰到好处的错误提示。
想亲自验证这些能力,可直接运行 angular 仓库中的form-validation文档示例(位于 adev/src/content/examples/form-validation),对照响应式与模板驱动两个版本的演员表单,观察同步校验、跨字段校验与异步校验各自的真实行为。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考