11 - 响应式表单
📖 学习目标
通过本章节学习,您将掌握:
- 响应式表单基础
- FormBuilder的使用
- FormGroup和FormControl
- 表单验证
- 自定义验证器
- 表单状态管理
- 复杂表单处理
🎯 核心概念
1. 响应式表单优势
- 类型安全:编译时类型检查
- 可测试性:易于单元测试
- 可预测性:状态变化可追踪
- 灵活性:支持复杂表单逻辑
- 性能:按需更新,性能更好
2. 表单结构
FormGroup
├── FormControl (单个字段)
├── FormArray (数组字段)
└── FormGroup (嵌套组)
3. 验证器类型
- 同步验证器:立即返回验证结果
- 异步验证器:返回Promise或Observable
- 内置验证器:required, email, min, max等
- 自定义验证器:业务逻辑验证
🔧 基础响应式表单
1. 启用ReactiveFormsModule
typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReactiveFormsModule // 导入ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
2. 基本表单创建
typescript
// user-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.css']
})
export class UserFormComponent implements OnInit {
userForm: FormGroup;
constructor() {
this.userForm = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(2)]),
email: new FormControl('', [Validators.required, Validators.email]),
age: new FormControl('', [Validators.required, Validators.min(18), Validators.max(100)]),
phone: new FormControl('', [Validators.pattern(/^[0-9]{11}$/)]),
address: new FormGroup({
street: new FormControl('', Validators.required),
city: new FormControl('', Validators.required),
zipCode: new FormControl('', [Validators.required, Validators.pattern(/^[0-9]{6}$/)])
})
});
}
ngOnInit() {
// 监听表单值变化
this.userForm.valueChanges.subscribe(value => {
console.log('Form value changed:', value);
});
// 监听表单状态变化
this.userForm.statusChanges.subscribe(status => {
console.log('Form status:', status);
});
}
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
// 处理表单提交
} else {
this.markFormGroupTouched(this.userForm);
}
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
} else {
control?.markAsTouched();
}
});
}
}
3. 表单模板
html
<!-- user-form.component.html -->
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">姓名:</label>
<input
id="name"
type="text"
formControlName="name"
class="form-control"
[class.is-invalid]="userForm.get('name')?.invalid && userForm.get('name')?.touched">
<div *ngIf="userForm.get('name')?.invalid && userForm.get('name')?.touched"
class="invalid-feedback">
<div *ngIf="userForm.get('name')?.errors?.['required']">姓名是必填项</div>
<div *ngIf="userForm.get('name')?.errors?.['minlength']">
姓名至少需要{{ userForm.get('name')?.errors?.['minlength'].requiredLength }}个字符
</div>
</div>
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
type="email"
formControlName="email"
class="form-control"
[class.is-invalid]="userForm.get('email')?.invalid && userForm.get('email')?.touched">
<div *ngIf="userForm.get('email')?.invalid && userForm.get('email')?.touched"
class="invalid-feedback">
<div *ngIf="userForm.get('email')?.errors?.['required']">邮箱是必填项</div>
<div *ngIf="userForm.get('email')?.errors?.['email']">请输入有效的邮箱地址</div>
</div>
</div>
<div class="form-group">
<label for="age">年龄:</label>
<input
id="age"
type="number"
formControlName="age"
class="form-control"
[class.is-invalid]="userForm.get('age')?.invalid && userForm.get('age')?.touched">
<div *ngIf="userForm.get('age')?.invalid && userForm.get('age')?.touched"
class="invalid-feedback">
<div *ngIf="userForm.get('age')?.errors?.['required']">年龄是必填项</div>
<div *ngIf="userForm.get('age')?.errors?.['min']">年龄不能小于18岁</div>
<div *ngIf="userForm.get('age')?.errors?.['max']">年龄不能大于100岁</div>
</div>
</div>
<!-- 嵌套表单组 -->
<div formGroupName="address">
<h3>地址信息</h3>
<div class="form-group">
<label for="street">街道:</label>
<input
id="street"
type="text"
formControlName="street"
class="form-control"
[class.is-invalid]="userForm.get('address.street')?.invalid && userForm.get('address.street')?.touched">
<div *ngIf="userForm.get('address.street')?.invalid && userForm.get('address.street')?.touched"
class="invalid-feedback">
街道是必填项
</div>
</div>
<div class="form-group">
<label for="city">城市:</label>
<input
id="city"
type="text"
formControlName="city"
class="form-control"
[class.is-invalid]="userForm.get('address.city')?.invalid && userForm.get('address.city')?.touched">
<div *ngIf="userForm.get('address.city')?.invalid && userForm.get('address.city')?.touched"
class="invalid-feedback">
城市是必填项
</div>
</div>
<div class="form-group">
<label for="zipCode">邮编:</label>
<input
id="zipCode"
type="text"
formControlName="zipCode"
class="form-control"
[class.is-invalid]="userForm.get('address.zipCode')?.invalid && userForm.get('address.zipCode')?.touched">
<div *ngIf="userForm.get('address.zipCode')?.invalid && userForm.get('address.zipCode')?.touched"
class="invalid-feedback">
请输入6位数字邮编
</div>
</div>
</div>
<button type="submit" [disabled]="userForm.invalid" class="btn btn-primary">
提交
</button>
</form>
🏗️ FormBuilder
1. 使用FormBuilder
typescript
// user-form-builder.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form-builder',
templateUrl: './user-form-builder.component.html'
})
export class UserFormBuilderComponent implements OnInit {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
// 基本信息
personalInfo: this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
phone: ['', [Validators.pattern(/^[0-9]{11}$/)]],
birthDate: ['', Validators.required]
}),
// 地址信息
address: this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required],
state: ['', Validators.required],
zipCode: ['', [Validators.required, Validators.pattern(/^[0-9]{6}$/)]],
country: ['', Validators.required]
}),
// 账户信息
account: this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, { validators: this.passwordMatchValidator }),
// 偏好设置
preferences: this.fb.group({
newsletter: [false],
notifications: [true],
theme: ['light', Validators.required],
language: ['zh', Validators.required]
})
});
}
ngOnInit() {
// 监听表单变化
this.userForm.valueChanges.subscribe(value => {
console.log('Form value changed:', value);
});
// 监听特定字段变化
this.userForm.get('personalInfo.firstName')?.valueChanges.subscribe(value => {
console.log('First name changed:', value);
});
}
// 密码匹配验证器
passwordMatchValidator(group: FormGroup) {
const password = group.get('password');
const confirmPassword = group.get('confirmPassword');
if (password && confirmPassword && password.value !== confirmPassword.value) {
confirmPassword.setErrors({ passwordMismatch: true });
return { passwordMismatch: true };
}
return null;
}
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
// 处理表单提交
} else {
this.markFormGroupTouched(this.userForm);
}
}
resetForm() {
this.userForm.reset();
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
} else {
control?.markAsTouched();
}
});
}
}
🔄 FormArray
1. 动态表单数组
typescript
// dynamic-form.component.ts
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 {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
// 动态技能数组
skills: this.fb.array([]),
// 动态工作经历数组
workExperience: this.fb.array([])
});
}
ngOnInit() {
// 添加初始技能
this.addSkill();
// 添加初始工作经历
this.addWorkExperience();
}
// 获取技能FormArray
get skillsArray(): FormArray {
return this.userForm.get('skills') as FormArray;
}
// 获取工作经历FormArray
get workExperienceArray(): FormArray {
return this.userForm.get('workExperience') as FormArray;
}
// 添加技能
addSkill() {
const skillGroup = this.fb.group({
name: ['', Validators.required],
level: ['beginner', Validators.required],
years: [0, [Validators.min(0), Validators.max(50)]]
});
this.skillsArray.push(skillGroup);
}
// 删除技能
removeSkill(index: number) {
this.skillsArray.removeAt(index);
}
// 添加工作经历
addWorkExperience() {
const workExpGroup = this.fb.group({
company: ['', Validators.required],
position: ['', Validators.required],
startDate: ['', Validators.required],
endDate: [''],
current: [false],
description: ['', Validators.maxLength(500)]
});
// 监听当前工作状态变化
workExpGroup.get('current')?.valueChanges.subscribe(isCurrent => {
const endDateControl = workExpGroup.get('endDate');
if (isCurrent) {
endDateControl?.clearValidators();
endDateControl?.setValue('');
} else {
endDateControl?.setValidators(Validators.required);
}
endDateControl?.updateValueAndValidity();
});
this.workExperienceArray.push(workExpGroup);
}
// 删除工作经历
removeWorkExperience(index: number) {
this.workExperienceArray.removeAt(index);
}
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
} else {
this.markFormGroupTouched(this.userForm);
}
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
} else if (control instanceof FormArray) {
control.controls.forEach(arrayControl => {
if (arrayControl instanceof FormGroup) {
this.markFormGroupTouched(arrayControl);
} else {
arrayControl.markAsTouched();
}
});
} else {
control?.markAsTouched();
}
});
}
}
2. FormArray模板
html
<!-- dynamic-form.component.html -->
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">姓名:</label>
<input id="name" type="text" formControlName="name" class="form-control">
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input id="email" type="email" formControlName="email" class="form-control">
</div>
<!-- 技能数组 -->
<div class="form-section">
<h3>技能</h3>
<div formArrayName="skills">
<div *ngFor="let skill of skillsArray.controls; let i = index"
[formGroupName]="i"
class="skill-item">
<div class="form-row">
<div class="form-group">
<label>技能名称:</label>
<input formControlName="name" class="form-control" placeholder="技能名称">
</div>
<div class="form-group">
<label>熟练程度:</label>
<select formControlName="level" class="form-control">
<option value="beginner">初级</option>
<option value="intermediate">中级</option>
<option value="advanced">高级</option>
<option value="expert">专家</option>
</select>
</div>
<div class="form-group">
<label>使用年限:</label>
<input formControlName="years" type="number" class="form-control" min="0" max="50">
</div>
<div class="form-group">
<button type="button" (click)="removeSkill(i)" class="btn btn-danger">删除</button>
</div>
</div>
</div>
</div>
<button type="button" (click)="addSkill()" class="btn btn-secondary">添加技能</button>
</div>
<!-- 工作经历数组 -->
<div class="form-section">
<h3>工作经历</h3>
<div formArrayName="workExperience">
<div *ngFor="let work of workExperienceArray.controls; let i = index"
[formGroupName]="i"
class="work-item">
<div class="form-group">
<label>公司名称:</label>
<input formControlName="company" class="form-control" placeholder="公司名称">
</div>
<div class="form-group">
<label>职位:</label>
<input formControlName="position" class="form-control" placeholder="职位">
</div>
<div class="form-row">
<div class="form-group">
<label>开始日期:</label>
<input formControlName="startDate" type="date" class="form-control">
</div>
<div class="form-group">
<label>结束日期:</label>
<input formControlName="endDate" type="date" class="form-control">
</div>
<div class="form-group">
<label>
<input formControlName="current" type="checkbox"> 当前工作
</label>
</div>
</div>
<div class="form-group">
<label>工作描述:</label>
<textarea formControlName="description" class="form-control" rows="3"
placeholder="工作描述"></textarea>
</div>
<button type="button" (click)="removeWorkExperience(i)" class="btn btn-danger">删除</button>
</div>
</div>
<button type="button" (click)="addWorkExperience()" class="btn btn-secondary">添加工作经历</button>
</div>
<button type="submit" [disabled]="userForm.invalid" class="btn btn-primary">提交</button>
</form>
🎯 自定义验证器
1. 同步验证器
typescript
// validators/custom.validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// 密码强度验证器
export function passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value) return null;
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumeric = /[0-9]/.test(value);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const valid = hasUpperCase && hasLowerCase && hasNumeric && hasSpecialChar;
return valid ? null : { passwordStrength: true };
};
}
// 年龄验证器
export function ageValidator(minAge: number, maxAge: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value) return null;
const age = new Date().getFullYear() - new Date(value).getFullYear();
if (age < minAge || age > maxAge) {
return { ageRange: { min: minAge, max: maxAge, actual: age } };
}
return null;
};
}
// 用户名验证器
export function usernameValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value) return null;
const valid = /^[a-zA-Z0-9_]{3,20}$/.test(value);
return valid ? null : { usernameFormat: true };
};
}
2. 异步验证器
typescript
// validators/async.validators.ts
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, delay } from 'rxjs/operators';
import { UserService } from '../services/user.service';
@Injectable({
providedIn: 'root'
})
export class AsyncValidators {
constructor(private userService: UserService) {}
// 用户名唯一性验证
usernameUniqueValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
return this.userService.checkUsernameExists(control.value).pipe(
map(exists => exists ? { usernameExists: true } : null),
catchError(() => of(null))
);
};
}
// 邮箱唯一性验证
emailUniqueValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
return this.userService.checkEmailExists(control.value).pipe(
map(exists => exists ? { emailExists: true } : null),
catchError(() => of(null))
);
};
}
}
3. 使用自定义验证器
typescript
// 在组件中使用
import { passwordStrengthValidator, ageValidator } from '../validators/custom.validators';
import { AsyncValidators } from '../validators/async.validators';
export class UserFormComponent {
constructor(
private fb: FormBuilder,
private asyncValidators: AsyncValidators
) {
this.userForm = this.fb.group({
username: ['',
[Validators.required, Validators.minLength(3)],
[this.asyncValidators.usernameUniqueValidator()]
],
email: ['',
[Validators.required, Validators.email],
[this.asyncValidators.emailUniqueValidator()]
],
password: ['', [
Validators.required,
Validators.minLength(8),
passwordStrengthValidator()
]],
birthDate: ['', [
Validators.required,
ageValidator(18, 100)
]]
});
}
}
🎮 实践练习
练习1:创建用户注册表单
创建一个完整的用户注册表单,包含:
- 基本信息(姓名、邮箱、密码)
- 地址信息(可添加多个地址)
- 工作经历(动态添加/删除)
- 技能标签(动态管理)
- 完整的验证和错误处理
练习2:实现动态问卷系统
创建一个动态问卷系统,支持:
- 动态添加/删除问题
- 多种问题类型(单选、多选、文本、评分)
- 条件显示问题
- 表单验证和提交
📚 详细示例
完整的用户注册表单
typescript
// user-registration.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
import { passwordStrengthValidator } from '../validators/custom.validators';
import { AsyncValidators } from '../validators/async.validators';
@Component({
selector: 'app-user-registration',
templateUrl: './user-registration.component.html',
styleUrls: ['./user-registration.component.css']
})
export class UserRegistrationComponent implements OnInit {
userForm: FormGroup;
isSubmitting = false;
constructor(
private fb: FormBuilder,
private asyncValidators: AsyncValidators
) {
this.userForm = this.fb.group({
// 基本信息
personalInfo: this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', [Validators.required, Validators.minLength(2)]],
email: ['',
[Validators.required, Validators.email],
[this.asyncValidators.emailUniqueValidator()]
],
phone: ['', [Validators.pattern(/^1[3-9]\d{9}$/)]],
birthDate: ['', Validators.required]
}),
// 账户信息
account: this.fb.group({
username: ['',
[Validators.required, Validators.minLength(3)],
[this.asyncValidators.usernameUniqueValidator()]
],
password: ['', [
Validators.required,
Validators.minLength(8),
passwordStrengthValidator()
]],
confirmPassword: ['', Validators.required]
}, { validators: this.passwordMatchValidator }),
// 地址信息
addresses: this.fb.array([]),
// 工作经历
workExperience: this.fb.array([]),
// 技能
skills: this.fb.array([]),
// 偏好设置
preferences: this.fb.group({
newsletter: [false],
notifications: [true],
theme: ['light', Validators.required],
language: ['zh', Validators.required]
}),
// 服务条款
agreeTerms: [false, Validators.requiredTrue]
});
}
ngOnInit() {
// 添加初始地址
this.addAddress();
// 添加初始工作经历
this.addWorkExperience();
// 添加初始技能
this.addSkill();
}
// 获取FormArray
get addressesArray(): FormArray {
return this.userForm.get('addresses') as FormArray;
}
get workExperienceArray(): FormArray {
return this.userForm.get('workExperience') as FormArray;
}
get skillsArray(): FormArray {
return this.userForm.get('skills') as FormArray;
}
// 密码匹配验证器
passwordMatchValidator(group: FormGroup) {
const password = group.get('password');
const confirmPassword = group.get('confirmPassword');
if (password && confirmPassword && password.value !== confirmPassword.value) {
confirmPassword.setErrors({ passwordMismatch: true });
return { passwordMismatch: true };
}
return null;
}
// 地址管理
addAddress() {
const addressGroup = this.fb.group({
type: ['home', Validators.required],
street: ['', Validators.required],
city: ['', Validators.required],
state: ['', Validators.required],
zipCode: ['', [Validators.required, Validators.pattern(/^[0-9]{6}$/)]],
country: ['', Validators.required],
isDefault: [false]
});
// 监听默认地址变化
addressGroup.get('isDefault')?.valueChanges.subscribe(isDefault => {
if (isDefault) {
this.addressesArray.controls.forEach(control => {
if (control !== addressGroup) {
control.get('isDefault')?.setValue(false);
}
});
}
});
this.addressesArray.push(addressGroup);
}
removeAddress(index: number) {
this.addressesArray.removeAt(index);
}
// 工作经历管理
addWorkExperience() {
const workExpGroup = this.fb.group({
company: ['', Validators.required],
position: ['', Validators.required],
startDate: ['', Validators.required],
endDate: [''],
current: [false],
description: ['', Validators.maxLength(500)]
});
// 监听当前工作状态变化
workExpGroup.get('current')?.valueChanges.subscribe(isCurrent => {
const endDateControl = workExpGroup.get('endDate');
if (isCurrent) {
endDateControl?.clearValidators();
endDateControl?.setValue('');
} else {
endDateControl?.setValidators(Validators.required);
}
endDateControl?.updateValueAndValidity();
});
this.workExperienceArray.push(workExpGroup);
}
removeWorkExperience(index: number) {
this.workExperienceArray.removeAt(index);
}
// 技能管理
addSkill() {
const skillGroup = this.fb.group({
name: ['', Validators.required],
level: ['beginner', Validators.required],
years: [0, [Validators.min(0), Validators.max(50)]]
});
this.skillsArray.push(skillGroup);
}
removeSkill(index: number) {
this.skillsArray.removeAt(index);
}
onSubmit() {
if (this.userForm.valid) {
this.isSubmitting = true;
// 模拟API调用
setTimeout(() => {
console.log('Form submitted:', this.userForm.value);
this.isSubmitting = false;
alert('注册成功!');
this.resetForm();
}, 2000);
} else {
this.markFormGroupTouched(this.userForm);
}
}
resetForm() {
this.userForm.reset();
// 重新添加初始项
this.addressesArray.clear();
this.workExperienceArray.clear();
this.skillsArray.clear();
this.addAddress();
this.addWorkExperience();
this.addSkill();
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
} else if (control instanceof FormArray) {
control.controls.forEach(arrayControl => {
if (arrayControl instanceof FormGroup) {
this.markFormGroupTouched(arrayControl);
} else {
arrayControl.markAsTouched();
}
});
} else {
control?.markAsTouched();
}
});
}
}
✅ 学习检查
完成本章节后,请确认您能够:
- [ ] 使用ReactiveFormsModule创建表单
- [ ] 使用FormBuilder构建复杂表单
- [ ] 使用FormGroup和FormControl
- [ ] 使用FormArray管理动态字段
- [ ] 创建自定义验证器
- [ ] 处理表单状态和验证
- [ ] 实现复杂的表单逻辑
🚀 下一步
完成本章节学习后,请继续学习12-状态管理。