跳到主要内容

表单处理

表单是 Web 应用中收集用户输入的核心方式。Angular 提供了两种表单处理方式:响应式表单和模板驱动表单。本章将详细介绍这两种方式的使用方法。

两种表单方式对比

特性响应式表单模板驱动表单
表单模型在组件类中显式定义在模板中隐式创建
数据流同步异步
可测试性高,无需渲染 UI低,需要渲染 UI
适用场景复杂表单、动态表单简单表单、快速开发
验证方式函数指令

响应式表单

响应式表单使用 FormControlFormGroupFormArray 来管理表单状态。

基本设置

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
selector: 'app-reactive-form',
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<!-- 单个表单控件 -->
<div>
<label>用户名:</label>
<input type="text" formControlName="username">
<p *ngIf="userForm.get('username')?.invalid && userForm.get('username')?.touched">
用户名不能为空,最少 3 个字符
</p>
</div>

<div>
<label>邮箱:</label>
<input type="email" formControlName="email">
<p *ngIf="userForm.get('email')?.errors?.['required'] && userForm.get('email')?.touched">
邮箱不能为空
</p>
<p *ngIf="userForm.get('email')?.errors?.['email']">
请输入有效的邮箱地址
</p>
</div>

<div>
<label>密码:</label>
<input type="password" formControlName="password">
</div>

<button type="submit" [disabled]="userForm.invalid">提交</button>
</form>

<pre>{{ userForm.value | json }}</pre>
`,
imports: [ReactiveFormsModule]
})
export class ReactiveFormComponent {
userForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.minLength(3)]),
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(6)])
});

onSubmit() {
if (this.userForm.valid) {
console.log('表单数据:', this.userForm.value);
}
}
}

使用 FormBuilder

FormBuilder 提供了更简洁的表单创建方式:

import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';

@Component({
selector: 'app-form-builder',
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<div formGroupName="personal">
<h4>个人信息</h4>
<div>
<label>姓名:</label>
<input type="text" formControlName="name">
</div>
<div>
<label>年龄:</label>
<input type="number" formControlName="age">
</div>
</div>

<div>
<label>邮箱:</label>
<input type="email" formControlName="email">
</div>

<div formArrayName="hobbies">
<h4>爱好</h4>
<button type="button" (click)="addHobby()">添加爱好</button>
@for (hobby of hobbies.controls; track $index) {
<div>
<input [formControlName]="$index">
<button type="button" (click)="removeHobby($index)">删除</button>
</div>
}
</div>

<button type="submit">提交</button>
</form>
`,
imports: [ReactiveFormsModule]
})
export class FormBuilderComponent {
private fb = inject(FormBuilder);

profileForm = this.fb.group({
personal: this.fb.group({
name: ['', Validators.required],
age: ['', [Validators.required, Validators.min(0), Validators.max(150)]]
}),
email: ['', [Validators.required, Validators.email]],
hobbies: this.fb.array([
this.fb.control('阅读')
])
});

get hobbies() {
return this.profileForm.get('hobbies') as FormArray;
}

addHobby() {
this.hobbies.push(this.fb.control(''));
}

removeHobby(index: number) {
this.hobbies.removeAt(index);
}

onSubmit() {
console.log('表单数据:', this.profileForm.value);
}
}

表单控件状态

每个表单控件都有以下状态属性:

@Component({
selector: 'app-control-state',
template: `
<input [formControl]="control"
[class.is-valid]="control.valid && control.touched"
[class.is-invalid]="control.invalid && control.touched">

<div *ngIf="control.touched">
<p>有效: {{ control.valid }}</p>
<p>无效: {{ control.invalid }}</p>
<p>已修改: {{ control.dirty }}</p>
<p>未修改: {{ control.pristine }}</p>
<p>已触碰: {{ control.touched }}</p>
<p>未触碰: {{ control.untouched }}</p>
<p>已启用: {{ control.enabled }}</p>
<p>已禁用: {{ control.disabled }}</p>
</div>
`,
imports: [ReactiveFormsModule]
})
export class ControlStateComponent {
control = new FormControl('');
}

内置验证器

import { Validators } from '@angular/forms';

// 常用内置验证器
const control = new FormControl('', [
Validators.required, // 必填
Validators.minLength(3), // 最小长度
Validators.maxLength(10), // 最大长度
Validators.min(0), // 最小值
Validators.max(100), // 最大值
Validators.email, // 邮箱格式
Validators.pattern('[a-z]+'), // 正则匹配
Validators.nullValidator, // 空验证器
]);

自定义验证器

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// 函数式验证器
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? { forbiddenName: { value: control.value } } : null;
};
}

// 使用自定义验证器
@Component({
selector: 'app-custom-validator',
template: `
<input [formControl]="nameControl">
<p *ngIf="nameControl.errors?.['forbiddenName']">
这个名字被禁止使用
</p>
`,
imports: [ReactiveFormsModule]
})
export class CustomValidatorComponent {
nameControl = new FormControl('', [
Validators.required,
forbiddenNameValidator(/admin/i)
]);
}

跨字段验证

import { FormGroup, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';

// 密码确认验证器
export const passwordMatchValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const password = control.get('password');
const confirmPassword = control.get('confirmPassword');

if (password && confirmPassword && password.value !== confirmPassword.value) {
return { passwordMismatch: true };
}
return null;
};

@Component({
selector: 'app-password-form',
template: `
<form [formGroup]="passwordForm" (ngSubmit)="onSubmit()">
<div>
<label>密码:</label>
<input type="password" formControlName="password">
</div>
<div>
<label>确认密码:</label>
<input type="password" formControlName="confirmPassword">
</div>
<p *ngIf="passwordForm.errors?.['passwordMismatch'] && passwordForm.touched">
两次输入的密码不一致
</p>
<button type="submit" [disabled]="passwordForm.invalid">提交</button>
</form>
`,
imports: [ReactiveFormsModule]
})
export class PasswordFormComponent {
passwordForm = new FormGroup({
password: new FormControl('', [Validators.required, Validators.minLength(6)]),
confirmPassword: new FormControl('', Validators.required)
}, { validators: passwordMatchValidator });

onSubmit() {
console.log('密码:', this.passwordForm.value.password);
}
}

动态表单

import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormGroup, FormArray } from '@angular/forms';

@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="orderForm" (ngSubmit)="onSubmit()">
<h3>订单表单</h3>

<div>
<label>客户名称:</label>
<input formControlName="customerName">
</div>

<div formArrayName="items">
<h4>订单项目</h4>
<button type="button" (click)="addItem()">添加项目</button>

@for (item of items.controls; track $index; let i = $index) {
<div [formGroupName]="i" class="item-row">
<input placeholder="商品名称" formControlName="productName">
<input type="number" placeholder="数量" formControlName="quantity">
<input type="number" placeholder="单价" formControlName="price">
<button type="button" (click)="removeItem(i)">删除</button>
<span>小计: {{ item.value.quantity * item.value.price }}</span>
</div>
}
</div>

<p>订单总额: {{ calculateTotal() }}</p>
<button type="submit">提交订单</button>
</form>
`,
imports: [ReactiveFormsModule]
})
export class DynamicFormComponent {
private fb = inject(FormBuilder);

orderForm = this.fb.group({
customerName: ['', Validators.required],
items: this.fb.array([])
});

get items() {
return this.orderForm.get('items') as FormArray;
}

createItem(): FormGroup {
return this.fb.group({
productName: ['', Validators.required],
quantity: [1, [Validators.required, Validators.min(1)]],
price: [0, [Validators.required, Validators.min(0)]]
});
}

addItem() {
this.items.push(this.createItem());
}

removeItem(index: number) {
this.items.removeAt(index);
}

calculateTotal(): number {
return this.items.controls.reduce((total, item) => {
return total + (item.value.quantity * item.value.price || 0);
}, 0);
}

onSubmit() {
console.log('订单数据:', this.orderForm.value);
}
}

模板驱动表单

模板驱动表单使用 ngModel 指令在模板中定义表单模型。

基本用法

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-template-form',
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<div>
<label>用户名:</label>
<input type="text"
name="username"
[(ngModel)]="user.username"
#username="ngModel"
required
minlength="3">
<p *ngIf="username.invalid && username.touched">
用户名至少 3 个字符
</p>
</div>

<div>
<label>邮箱:</label>
<input type="email"
name="email"
[(ngModel)]="user.email"
#email="ngModel"
required
email>
<p *ngIf="email.errors?.['required'] && email.touched">
邮箱不能为空
</p>
<p *ngIf="email.errors?.['email']">
请输入有效的邮箱
</p>
</div>

<button type="submit" [disabled]="userForm.invalid">提交</button>
</form>

<pre>{{ user | json }}</pre>
`,
imports: [FormsModule]
})
export class TemplateFormComponent {
user = {
username: '',
email: ''
};

onSubmit(form: NgForm) {
console.log('表单数据:', form.value);
}
}

ngModelGroup

使用 ngModelGroup 对表单字段分组:

@Component({
selector: 'app-template-group',
template: `
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
<div ngModelGroup="address" #address="ngModelGroup">
<h4>地址信息</h4>
<div>
<label>省份:</label>
<input name="province" [(ngModel)]="address.province" required>
</div>
<div>
<label>城市:</label>
<input name="city" [(ngModel)]="address.city" required>
</div>
<div>
<label>详细地址:</label>
<input name="detail" [(ngModel)]="address.detail" required>
</div>
</div>

<button type="submit">提交</button>
</form>
`,
imports: [FormsModule]
})
export class TemplateGroupComponent {
address = {
province: '',
city: '',
detail: ''
};

onSubmit(form: NgForm) {
console.log('表单值:', form.value);
// 输出: { address: { province: '...', city: '...', detail: '...' } }
}
}

小结

  1. 响应式表单适合复杂表单,在组件类中定义模型,可测试性好
  2. 模板驱动表单适合简单表单,在模板中定义模型,开发速度快
  3. FormControl 管理单个表单控件,FormGroup 管理控件组,FormArray 管理动态控件列表
  4. 验证器用于验证用户输入,支持内置验证器和自定义验证器
  5. 动态表单可以根据数据动态生成表单结构

下一步

掌握了表单处理后,接下来学习 路由系统,了解如何实现页面导航。