1. 两种表单模式对比
| 特性 | 模板驱动表单 | 响应式表单 |
|---|---|---|
| 数据绑定 | 双向绑定(ngModel) | 单向绑定(FormControl) |
| 验证定义 | HTML 属性 | TypeScript 代码 |
| 可测试性 | 较难(依赖 DOM) | 易于单元测试 |
| 动态表单 | 较复杂 | 原生支持 FormArray |
| 适用场景 | 简单登录/搜索表单 | 复杂业务表单、动态字段 |
推荐选择:对于新项目和复杂业务表单,优先使用响应式表单(Reactive Forms)——它的表单逻辑完全在 TypeScript 中,可测试性强,与 RxJS 和 Signals 集成更好。
2. 模板驱动表单
模板驱动表单通过 HTML 属性定义验证规则,适合简单场景。需要导入 FormsModule。
// login.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
imports: [FormsModule],
template: `
<form (ngSubmit)="onSubmit(loginForm)" #loginForm="ngForm">
<div>
<label>邮箱</label>
<input
type="email"
name="email"
[(ngModel)]="formData.email"
required email
#emailRef="ngModel"
>
@if (emailRef.invalid && emailRef.touched) {
@if (emailRef.errors?.['required']) {
<span class="error">邮箱不能为空</span>
}
@if (emailRef.errors?.['email']) {
<span class="error">邮箱格式不正确</span>
}
}
</div>
<div>
<label>密码</label>
<input
type="password"
name="password"
[(ngModel)]="formData.password"
required minlength="6"
#pwdRef="ngModel"
>
@if (pwdRef.invalid && pwdRef.touched) {
<span class="error">密码至少 6 位</span>
}
</div>
<button type="submit" [disabled]="loginForm.invalid">登录</button>
</form>
`
})
export class LoginComponent {
formData = { email: '', password: '' };
onSubmit(form: NgForm) {
if (form.valid) {
console.log('提交:', this.formData);
}
}
}
3. 响应式表单
响应式表单的逻辑完全在 TypeScript 类中定义,模板只负责绑定,更易维护和测试。
FormBuilder + Validators
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-register',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './register.component.html'
})
export class RegisterComponent {
private fb = inject(FormBuilder);
private authService = inject(AuthService);
form = this.fb.group({
name: ['', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(50)
]],
email: ['', [
Validators.required,
Validators.email
], [this.emailExistsValidator()]], // 第三个参数:异步验证器
password: ['', [
Validators.required,
Validators.minLength(8),
passwordStrengthValidator // 自定义验证器
]],
confirmPassword: ['', Validators.required],
agreeTerms: [false, Validators.requiredTrue]
}, {
validators: passwordMatchValidator // 跨字段验证器
});
// 快捷访问各字段
get name() { return this.form.get('name')!; }
get email() { return this.form.get('email')!; }
onSubmit() {
if (this.form.valid) {
const { name, email, password } = this.form.value;
this.authService.register({ name: name!, email: email!, password: password! })
.subscribe();
} else {
this.form.markAllAsTouched(); // 触发所有字段的验证显示
}
}
}
模板绑定
<!-- register.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="field">
<label>用户名</label>
<input formControlName="name" type="text">
@if (name.invalid && name.touched) {
@if (name.errors?.['required']) { <span class="error">必填</span> }
@if (name.errors?.['minlength']) { <span class="error">至少 2 个字符</span> }
}
</div>
<div class="field">
<label>邮箱</label>
<input formControlName="email" type="email">
@if (email.pending) { <span>检查中...</span> }
@if (email.errors?.['emailExists']) { <span class="error">邮箱已被注册</span> }
</div>
<button type="submit" [disabled]="form.invalid || form.pending">
注册
</button>
</form>
4. 自定义验证器
import { AbstractControl, ValidationErrors, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap } from 'rxjs/operators';
// 同步自定义验证器:密码强度
export function passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
const value: string = control.value || '';
const hasUpperCase = /[A-Z]+/.test(value);
const hasLowerCase = /[a-z]+/.test(value);
const hasNumber = /[0-9]+/.test(value);
const isValid = hasUpperCase && hasLowerCase && hasNumber;
return isValid ? null : { passwordStrength: true };
}
// 跨字段验证器:确认密码匹配
export function passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return password === confirmPassword ? null : { passwordMismatch: true };
}
// 异步验证器:检查邮箱是否已被注册
export function emailExistsValidator(authService: AuthService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return authService.checkEmailExists(control.value).pipe(
debounceTime(400),
map(exists => exists ? { emailExists: true } : null),
catchError(() => of(null))
);
};
}
5. FormArray — 动态表单字段
@Component({
selector: 'app-skills-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form">
<div formArrayName="skills">
@for (control of skillsArray.controls; track $index) {
<div>
<input [formControlName]="$index" placeholder="技能名称">
<button type="button" (click)="removeSkill($index)">删除</button>
</div>
}
</div>
<button type="button" (click)="addSkill()">添加技能</button>
</form>
`
})
export class SkillsFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
skills: this.fb.array([
this.fb.control('TypeScript', Validators.required)
])
});
get skillsArray() {
return this.form.get('skills') as FormArray;
}
addSkill() {
this.skillsArray.push(this.fb.control('', Validators.required));
}
removeSkill(index: number) {
this.skillsArray.removeAt(index);
}
}
本章小结:Angular 提供模板驱动和响应式两种表单方案。响应式表单适合大多数业务场景,支持同步/异步验证器、FormArray 动态字段,且易于测试。下一章深入 RxJS 响应式编程。