本章目标:掌握Angular响应式表单的核心概念,学会创建复杂表单、实现表单验证、处理动态表单和表单数组等高级功能。
响应式表单三大核心
FormControl
单个表单控件
FormGroup
表单控件组
FormArray
表单控件数组
什么是响应式表单?
响应式表单是Angular提供的两种表单处理方式之一,它使用显式的、不可变的方式来管理表单状态,提供了更强的类型安全和更灵活的表单控制。
响应式表单特点
- 在组件类中显式创建表单模型
- 表单状态不可变,变化时返回新对象
- 支持同步和异步验证
- 提供强大的类型检查和自动补全
- 适合复杂表单场景
与模板驱动表单对比
| 特性 | 响应式表单 | 模板驱动表单 |
|---|---|---|
| 表单模型 | 显式(组件中) | 隐式(模板中) |
| 验证方式 | 函数式验证器 | 指令验证器 |
| 可变性 | 不可变 | 可变 |
| 复杂度 | 适合复杂表单 | 适合简单表单 |
1. 创建响应式表单
响应式表单需要从@angular/forms模块导入ReactiveFormsModule。
// 1. 在模块中导入ReactiveFormsModule
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
// ... 其他导入
ReactiveFormsModule // 引入响应式表单模块
],
// ...
})
export class AppModule { }
// 2. 在组件中创建表单
import { Component, OnInit } from '@angular/core';
import {
FormBuilder,
FormGroup,
FormControl,
Validators,
FormArray
} from '@angular/forms';
@Component({
selector: 'app-reactive-form',
templateUrl: './reactive-form.component.html'
})
export class ReactiveFormComponent implements OnInit {
// 表单组定义
userForm: FormGroup;
// 使用FormBuilder注入(推荐)
constructor(private fb: FormBuilder) {
// 在构造函数中初始化表单
this.userForm = this.fb.group({
// 表单控件定义
});
}
ngOnInit(): void {
// 或者在这里初始化表单
this.initForm();
}
private initForm(): void {
this.userForm = this.fb.group({
// 表单控件将在后面详细定义
});
}
}
使用FormBuilder创建表单
FormBuilder是创建表单的便捷工具,语法更简洁:
// 使用FormBuilder创建基本表单
this.userForm = this.fb.group({
// 文本输入框
username: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(20)
]],
// 邮箱输入框
email: ['', [
Validators.required,
Validators.email
]],
// 密码输入框
password: ['', [
Validators.required,
Validators.minLength(8),
Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$')
]],
// 选择框
gender: ['male', Validators.required],
// 复选框
agreeTerms: [false, Validators.requiredTrue],
// 地址组(嵌套FormGroup)
address: this.fb.group({
street: [''],
city: ['', Validators.required],
zipCode: ['', [
Validators.required,
Validators.pattern('^\\d{5,6}$')
]]
}),
// 兴趣数组(FormArray)
hobbies: this.fb.array([
this.fb.control('')
])
});
基础表单实时演示
2. 表单控件类型
响应式表单提供三种核心类型:FormControl、FormGroup和FormArray。
表单控件层级结构
FormGroup
顶级表单容器
FormGroup
嵌套表单组(地址)
FormControl
表单字段(用户名)
FormControl
管理单个表单控件的值和验证状态。
// 创建FormControl的三种方式
const control1 = new FormControl(); // 空值
const control2 = new FormControl('初始值'); // 带初始值
const control3 = new FormControl('', [
Validators.required,
Validators.email
]); // 带初始值和验证器
// 获取和设置值
const value = control1.value; // 获取值
control1.setValue('新值'); // 设置值
control1.patchValue('部分更新值'); // 部分更新
// 监听值变化
control1.valueChanges.subscribe(newValue => {
console.log('值已变化:', newValue);
});
// 监听状态变化
control1.statusChanges.subscribe(status => {
console.log('状态已变化:', status);
});
// 获取错误信息
const errors = control1.errors;
// 检查状态
const isValid = control1.valid;
const isInvalid = control1.invalid;
const isPending = control1.pending;
const isDirty = control1.dirty;
const isPristine = control1.pristine;
const isTouched = control1.touched;
const isUntouched = control1.untouched;
FormGroup
管理一组FormControl的集合,用于分组相关的表单字段。
// 创建FormGroup
const userGroup = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
age: new FormControl(0)
});
// 使用FormBuilder创建
const userGroup = this.fb.group({
firstName: '',
lastName: '',
age: 0
});
// 获取和设置值
const groupValue = userGroup.value; // 获取整个组的值
userGroup.setValue({ // 设置整个组的值(必须包含所有控件)
firstName: '张',
lastName: '三',
age: 25
});
userGroup.patchValue({ // 部分更新组的值
firstName: '李'
});
// 获取单个控件
const firstNameControl = userGroup.get('firstName');
// 监听组的值变化
userGroup.valueChanges.subscribe(value => {
console.log('表单组值变化:', value);
});
// 检查组的状态
const isGroupValid = userGroup.valid;
const isGroupInvalid = userGroup.invalid;
// 获取嵌套组
const addressGroup = userGroup.get('address') as FormGroup;
FormArray
管理动态数量的FormControl,用于创建列表或数组类型的表单字段。
// 创建FormArray
const skills = new FormArray([
new FormControl('Angular'),
new FormControl('TypeScript')
]);
// 使用FormBuilder创建
const skills = this.fb.array([
this.fb.control('Angular'),
this.fb.control('TypeScript')
]);
// 添加新控件
skills.push(new FormControl('RxJS'));
skills.push(this.fb.control('Node.js'));
// 插入控件
skills.insert(1, new FormControl('React'));
// 移除控件
skills.removeAt(0); // 移除指定索引的控件
skills.removeAt(skills.length - 1); // 移除最后一个控件
// 清空数组
skills.clear();
// 获取控件
const firstSkill = skills.at(0); // 获取第一个控件
// 获取所有控件
const allControls = skills.controls;
// 获取数组长度
const arrayLength = skills.length;
// 设置值
skills.setValue(['Vue', 'Svelte']);
// 监听值变化
skills.valueChanges.subscribe(value => {
console.log('技能列表变化:', value);
});
// 检查验证状态
const isValid = skills.valid;
3. 表单验证
响应式表单提供强大的验证功能,包括内置验证器和自定义验证器。
| 内置验证器 | 用途 | 示例 |
|---|---|---|
| Validators.required | 必填字段验证 | Validators.required |
| Validators.email | 邮箱格式验证 | Validators.email |
| Validators.minLength | 最小长度验证 | Validators.minLength(3) |
| Validators.maxLength | 最大长度验证 | Validators.maxLength(20) |
| Validators.pattern | 正则表达式验证 | Validators.pattern('[a-zA-Z ]*') |
| Validators.min | 最小值验证 | Validators.min(0) |
| Validators.max | 最大值验证 | Validators.max(100) |
| Validators.requiredTrue | 必须为true(用于复选框) | Validators.requiredTrue |
自定义验证器
创建自定义验证器来处理特定业务逻辑:
// 1. 同步验证器(返回ValidationErrors或null)
export function passwordStrengthValidator(control: FormControl) {
const value = control.value || '';
if (!value) {
return null; // 如果没有值,不验证
}
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const errors: any = {};
if (!hasUpperCase) {
errors.noUpperCase = '密码必须包含大写字母';
}
if (!hasLowerCase) {
errors.noLowerCase = '密码必须包含小写字母';
}
if (!hasNumber) {
errors.noNumber = '密码必须包含数字';
}
if (!hasSpecialChar) {
errors.noSpecialChar = '密码必须包含特殊字符';
}
// 如果密码长度小于8,也返回错误
if (value.length < 8) {
errors.minLength = '密码长度至少8位';
}
// 如果没有任何错误,返回null
return Object.keys(errors).length ? errors : null;
}
// 2. 异步验证器(返回Promise或Observable)
export function usernameAvailabilityValidator(
userService: UserService
): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const username = control.value;
if (!username) {
return of(null); // 如果没有值,不验证
}
return userService.checkUsernameAvailability(username).pipe(
map(isAvailable => {
return isAvailable ? null : { usernameTaken: '用户名已被占用' };
}),
catchError(() => of(null)) // 发生错误时不显示验证错误
);
};
}
// 3. 在表单中使用验证器
this.userForm = this.fb.group({
username: ['', [
Validators.required,
Validators.minLength(3)
], [
usernameAvailabilityValidator(this.userService) // 异步验证器
]],
password: ['', [
Validators.required,
passwordStrengthValidator // 自定义同步验证器
]],
confirmPassword: ['', Validators.required]
}, {
// 表单级别的验证器(验证多个字段的关系)
validators: [passwordMatchValidator]
});
// 4. 表单级别验证器(验证多个字段的关系)
export function passwordMatchValidator(group: FormGroup) {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return password === confirmPassword ? null : { passwordMismatch: true };
}
在模板中显示验证错误
<!-- 响应式表单模板 -->
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<!-- 用户名输入 -->
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username"
[class.is-invalid]="username.invalid && username.touched"
[class.is-valid]="username.valid && username.touched">
<!-- 验证错误信息 -->
<div *ngIf="username.invalid && username.touched" class="error-message">
<div *ngIf="username.errors?.['required']">
用户名是必填项
</div>
<div *ngIf="username.errors?.['minlength']">
用户名至少需要{{username.errors?.['minlength']?.requiredLength}}个字符
</div>
<div *ngIf="username.errors?.['usernameTaken']">
{{username.errors?.['usernameTaken']}}
</div>
<div *ngIf="username.errors?.['maxlength']">
用户名不能超过{{username.errors?.['maxlength']?.requiredLength}}个字符
>/div>
</div>
<!-- 显示验证状态 -->
<div class="form-text">
<span *ngIf="username.pending" class="text-warning">
<i class="fas fa-spinner fa-spin"></i> 验证中...
</span>
<span *ngIf="username.valid && username.touched" class="text-success">
<i class="fas fa-check-circle"></i> 用户名可用
</span>
</div>
</div>
<!-- 密码输入 -->
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input
type="password"
id="password"
class="form-control"
formControlName="password"
[class.is-invalid]="password.invalid && password.touched">
<div *ngIf="password.invalid && password.touched" class="error-message">
<div *ngIf="password.errors?.['required']">密码是必填项</div>
<div *ngIf="password.errors?.['noUpperCase']">
{{password.errors?.['noUpperCase']}}
</div>
<div *ngIf="password.errors?.['noLowerCase']">
{{password.errors?.['noLowerCase']}}
</div>
<div *ngIf="password.errors?.['noNumber']">
{{password.errors?.['noNumber']}}
</div>
<div *ngIf="password.errors?.['minLength']">
{{password.errors?.['minLength']}}
</div>
</div>
</div>
<!-- 确认密码输入 -->
<div class="mb-3">
<label for="confirmPassword" class="form-label">确认密码</label>
<input
type="password"
id="confirmPassword"
class="form-control"
formControlName="confirmPassword"
[class.is-invalid]="userForm.errors?.['passwordMismatch'] &&
confirmPassword.touched">
<div *ngIf="userForm.errors?.['passwordMismatch'] &&
confirmPassword.touched" class="error-message">
两次输入的密码不一致
</div>
</div>
<!-- 提交按钮 -->
<button
type="submit"
class="btn btn-primary"
[disabled]="userForm.invalid">
提交
</button>
</form>
4. 动态表单
使用FormArray创建动态表单,允许用户动态添加/删除表单字段。
动态表单优势
动态添加字段
用户可以根据需要添加更多输入字段
动态删除字段
用户可以删除不需要的字段
灵活的表单结构
适应不同业务需求的动态表单
验证每个字段
为每个动态字段单独验证
动态表单实现
// 组件代码 - 动态表单实现
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
@Component({
selector: 'app-dynamic-form',
templateUrl: './dynamic-form.component.html'
})
export class DynamicFormComponent implements OnInit {
dynamicForm: FormGroup;
constructor(private fb: FormBuilder) {
this.dynamicForm = this.fb.group({
// 其他字段...
experiences: this.fb.array([]) // 空的FormArray
});
}
ngOnInit(): void {
// 初始化时添加一个经验字段
this.addExperience();
}
// 获取experiences FormArray
get experiences(): FormArray {
return this.dynamicForm.get('experiences') as FormArray;
}
// 创建单个经验表单组
createExperience(): FormGroup {
return this.fb.group({
company: ['', Validators.required],
position: ['', Validators.required],
startDate: ['', Validators.required],
endDate: [''],
current: [false],
description: ['']
});
}
// 添加经验字段
addExperience(): void {
this.experiences.push(this.createExperience());
}
// 移除经验字段
removeExperience(index: number): void {
this.experiences.removeAt(index);
}
// 清空所有经验字段
clearExperiences(): void {
while (this.experiences.length !== 0) {
this.experiences.removeAt(0);
}
}
// 移动经验字段位置
moveExperienceUp(index: number): void {
if (index > 0) {
const experience = this.experiences.at(index);
this.experiences.removeAt(index);
this.experiences.insert(index - 1, experience);
}
}
moveExperienceDown(index: number): void {
if (index < this.experiences.length - 1) {
const experience = this.experiences.at(index);
this.experiences.removeAt(index);
this.experiences.insert(index + 1, experience);
}
}
// 表单提交
onSubmit(): void {
if (this.dynamicForm.valid) {
console.log('表单数据:', this.dynamicForm.value);
// 提交到服务器...
} else {
// 标记所有字段为touched以显示错误
this.markFormGroupTouched(this.dynamicForm);
}
}
// 标记所有字段为touched
private markFormGroupTouched(formGroup: FormGroup | FormArray): void {
Object.values(formGroup.controls).forEach(control => {
control.markAsTouched();
if (control instanceof FormGroup || control instanceof FormArray) {
this.markFormGroupTouched(control);
}
});
}
// 重置表单
resetForm(): void {
this.dynamicForm.reset();
this.clearExperiences();
this.addExperience(); // 重新添加一个空字段
}
}
动态表单模板
<form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()">
<!-- 工作经验动态字段 -->
<div formArrayName="experiences">
<h4>工作经验</h4>
<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-primary me-2"
(click)="addExperience()">
<i class="fas fa-plus me-1"></i>添加经历
</button>
<button type="button" class="btn btn-sm btn-outline-danger"
(click)="clearExperiences()"
[disabled]="experiences.length === 0">
<i class="fas fa-trash me-1"></i>清空所有
</button>
</div>
<!-- 动态生成经验字段 -->
<div *ngFor="let experience of experiences.controls; let i = index"
[formGroupName]="i"
class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>工作经验 {{ i + 1 }}</span>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary me-1"
(click)="moveExperienceUp(i)"
[disabled]="i === 0">
<i class="fas fa-arrow-up"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary me-1"
(click)="moveExperienceDown(i)"
[disabled]="i === experiences.length - 1">
<i class="fas fa-arrow-down"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger"
(click)="removeExperience(i)"
[disabled]="experiences.length === 1">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">公司名称</label>
<input type="text" class="form-control"
formControlName="company"
[class.is-invalid]="experience.get('company')?.invalid &&
experience.get('company')?.touched">
<div *ngIf="experience.get('company')?.invalid &&
experience.get('company')?.touched" class="error-message">
公司名称是必填项
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">职位</label>
<input type="text" class="form-control"
formControlName="position"
[class.is-invalid]="experience.get('position')?.invalid &&
experience.get('position')?.touched">
<div *ngIf="experience.get('position')?.invalid &&
experience.get('position')?.touched" class="error-message">
职位是必填项
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">开始日期</label>
<input type="date" class="form-control"
formControlName="startDate"
[class.is-invalid]="experience.get('startDate')?.invalid &&
experience.get('startDate')?.touched">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">结束日期</label>
<input type="date" class="form-control"
formControlName="endDate"
[disabled]="experience.get('current')?.value">
</div>
</div>
<div class="col-md-12">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox"
formControlName="current" id="current{{i}}">
<label class="form-check-label" for="current{{i}}">
目前在此工作
</label>
</div>
</div>
<div class="col-md-12">
<div class="mb-3">
<label class="form-label">工作描述</label>
<textarea class="form-control" rows="3"
formControlName="description"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 表单提交按钮 -->
<div class="mt-4">
<button type="submit" class="btn btn-primary me-2"
[disabled]="dynamicForm.invalid">
提交表单
</button>
<button type="button" class="btn btn-secondary"
(click)="resetForm()">
重置表单
</button>
</div>
</form>
5. 表单状态管理
响应式表单提供了丰富的状态管理功能,可以监控表单的变化过程。
表单状态类型
VALID / INVALID
表单验证状态:有效/无效
form.valid / form.invalid
PENDING
异步验证进行中
form.pending
DIRTY / PRISTINE
表单是否被修改过:已修改/未修改
form.dirty / form.pristine
TOUCHED / UNTOUCHED
表单是否被访问过:已访问/未访问
form.touched / form.untouched
监听表单状态变化
// 监听表单值变化
this.userForm.valueChanges.subscribe(value => {
console.log('表单值变化:', value);
// 可以在这里执行实时验证或自动保存
});
// 监听表单状态变化
this.userForm.statusChanges.subscribe(status => {
console.log('表单状态变化:', status);
switch(status) {
case 'VALID':
console.log('表单验证通过');
break;
case 'INVALID':
console.log('表单验证失败');
break;
case 'PENDING':
console.log('表单验证中...');
break;
case 'DISABLED':
console.log('表单已禁用');
break;
}
});
// 监听特定字段变化
const usernameControl = this.userForm.get('username');
usernameControl?.valueChanges.subscribe(value => {
console.log('用户名变化:', value);
});
usernameControl?.statusChanges.subscribe(status => {
console.log('用户名验证状态:', status);
});
// 获取表单状态快照
getFormSnapshot(): void {
const snapshot = {
value: this.userForm.value,
status: this.userForm.status,
valid: this.userForm.valid,
invalid: this.userForm.invalid,
pending: this.userForm.pending,
dirty: this.userForm.dirty,
pristine: this.userForm.pristine,
touched: this.userForm.touched,
untouched: this.userForm.untouched,
errors: this.userForm.errors
};
console.log('表单快照:', snapshot);
}
// 重置表单状态
resetFormState(): void {
// 重置表单值但保持状态
this.userForm.reset();
// 重置表单值到初始状态
this.userForm.reset({
username: '',
email: '',
gender: 'male'
});
// 重置并清除状态标记
this.userForm.reset({}, { emitEvent: false });
this.userForm.markAsPristine();
this.userForm.markAsUntouched();
}
// 启用/禁用表单
toggleForm(): void {
if (this.userForm.disabled) {
this.userForm.enable();
} else {
this.userForm.disable();
}
}
// 启用/禁用特定字段
toggleField(fieldName: string): void {
const control = this.userForm.get(fieldName);
if (control?.disabled) {
control.enable();
} else {
control?.disable();
}
}
表单状态实时监控
响应式表单最佳实践
推荐做法
- 使用
FormBuilder创建表单,代码更简洁 - 为复杂表单创建自定义验证器
- 使用
valueChanges进行实时验证和交互 - 为动态表单使用
FormArray - 在模板中正确显示验证错误信息
- 使用
markAsTouched在提交时显示所有错误 - 合理使用表单状态(dirty、pristine等)
- 为表单创建可复用的组件
应避免的做法
- 避免在模板中编写复杂逻辑
- 不要忘记取消订阅
valueChanges - 避免过度验证影响用户体验
- 不要在组件中直接修改表单值(使用setValue/patchValue)
- 避免创建过于庞大的表单组
- 不要忽略异步验证的处理
- 避免在验证器中执行耗时操作
- 不要忘记处理表单提交失败的情况
常用表单模式速查表
| 模式 | 用途 | 示例 |
|---|---|---|
| 基础表单 | 简单数据收集 | this.fb.group({username: ''}) |
| 嵌套表单 | 复杂数据结构 | address: this.fb.group({...}) |
| 动态表单 | 可变字段数量 | this.fb.array([...]) |
| 条件表单 | 根据条件显示字段 | 使用valueChanges监听 |
| 分步表单 | 多步骤表单流程 | 多个FormGroup + 路由 |
| 搜索表单 | 实时搜索过滤 | valueChanges + debounceTime |
| 上传表单 | 文件上传 | FormData + HttpClient |
| 向导表单 | 多页面表单向导 | 状态管理 + 路由守卫 |
本章总结
通过本章学习,你应该掌握了:
- 响应式表单创建:使用
ReactiveFormsModule和FormBuilder - 表单控件类型:
FormControl、FormGroup、FormArray - 表单验证:内置验证器、自定义验证器、异步验证器
- 动态表单:使用
FormArray创建可增删的表单字段 - 表单状态管理:监听表单变化、管理表单状态
- 模板集成:在模板中显示表单和验证错误
- 最佳实践:编写高效、可维护的响应式表单