表单
Angular 提供 响应式表单(Reactive Forms) 与 模板驱动表单(Template-driven)。复杂表单推荐 Reactive Forms(模型在 TypeScript 中,易测试)。见 组件与模板。
核心概念
两种表单方案的本质区别在于**「表单模型的定义位置」**:
- Reactive Forms:在组件类中用
FormControl/FormGroup/FormArray显式构建表单模型,模板只负责绑定formControlName,逻辑集中、易单测、类型可推断。 - Template-driven:表单模型隐式地由模板中的
[(ngModel)]生成,写法更接近传统 HTML 表单,适合简单场景,但复杂校验和动态字段处理起来不如 Reactive 直观。
响应式表单
import { Component } from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
@if (form.controls.email.invalid && form.controls.email.touched) {
<span>邮箱无效</span>
}
<button type="submit" [disabled]="form.invalid">提交</button>
</form>
`,
})
export class LoginComponent {
form = new FormGroup({
email: new FormControl("", { nonNullable: true, validators: [Validators.required, Validators.email] }),
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.getRawValue());
}
}
}
FormArray:动态字段列表
form = new FormGroup({
contacts: new FormArray([this.createContact()]),
});
createContact() {
return new FormGroup({
name: new FormControl("", { nonNullable: true, validators: [Validators.required] }),
phone: new FormControl("", { nonNullable: true }),
});
}
get contacts() {
return this.form.controls.contacts;
}
addContact() {
this.contacts.push(this.createContact());
}
removeContact(index: number) {
this.contacts.removeAt(index);
}
<div formArrayName="contacts">
@for (group of contacts.controls; track $index; let i = $index) {
<div [formGroupName]="i">
<input formControlName="name" placeholder="姓名" />
<input formControlName="phone" placeholder="电话" />
<button type="button" (click)="removeContact(i)">删除</button>
</div>
}
</div>
<button type="button" (click)="addContact()">新增联系人</button>
FormArray 适合处理「联系人列表」「多个地址」等数量不固定的字段组,每一项本身可以是 FormGroup。
自定义校验器
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
export function passwordMatch(passwordKey: string, confirmKey: string): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get(passwordKey)?.value;
const confirm = group.get(confirmKey)?.value;
return password === confirm ? null : { passwordMismatch: true };
};
}
// 使用
form = new FormGroup(
{
password: new FormControl("", { nonNullable: true }),
confirm: new FormControl("", { nonNullable: true }),
},
{ validators: passwordMatch("password", "confirm") }
);
异步校验器(如检查用户名是否已被占用)返回 Observable<ValidationErrors | null>,配置在 asyncValidators 中,Angular 会自动在输入停止一段时间后调用并管理 pending 状态。
模板驱动表单
在组件中导入 FormsModule,模板使用 [(ngModel)] 与 #f="ngForm":
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
<input name="email" [(ngModel)]="email" required email />
<button type="submit" [disabled]="f.invalid">提交</button>
</form>
适合简单场景(如单个搜索框、几个字段的设置表单);大型表单仍建议 Reactive,避免校验逻辑散落在模板属性中难以测试。
类型安全(Typed Forms)
Angular 14+ 支持为 FormGroup 指定泛型类型,减少 get('email') 的类型丢失:
interface LoginForm {
email: FormControl<string>;
remember: FormControl<boolean>;
}
const form = new FormGroup<LoginForm>({
email: new FormControl("", { nonNullable: true }),
remember: new FormControl(false, { nonNullable: true }),
});
form.controls.email.value; // string,而非 any
详见 官方 Typed Forms。
与后端的配合
提交前校验 form.valid;异步校验可用 AsyncValidator。与 HttpClient 结合发送 POST 请求:
onSubmit() {
if (this.form.invalid) {
this.form.markAllAsTouched(); // 显示所有字段的校验错误
return;
}
this.http.post("/api/login", this.form.getRawValue()).subscribe({
next: () => this.router.navigate(["/home"]),
error: (err) => this.serverError.set(err.error?.message ?? "登录失败"),
});
}
最佳实践
- 复杂表单(多步骤、动态字段、跨字段校验)一律用 Reactive Forms + Typed Forms。
- 校验器写成纯函数(如
passwordMatch),便于复用与单测,避免写在组件方法里。 - 提交前调用
markAllAsTouched(),确保未曾聚焦过的无效字段也显示错误提示。 - 用
nonNullable: true明确控件值不为null,配合 Typed Forms 获得准确类型推断。 - 表单状态(
valid/pending/dirty)用于控制提交按钮禁用态与 loading 态,避免重复提交。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
get('email') 返回类型是 any | 未使用 Typed Forms(旧版 API 或未标注泛型) | 升级到 Angular 14+ 并显式声明控件类型 |
| 校验错误一开始就显示 | 未判断 touched/dirty 就直接显示错误 | 配合 @if (control.invalid && control.touched) |
FormArray 增删后模板渲染错位 | @for 缺少稳定 track | 用索引或控件内部 id 作为 track(数组本身可变时用索引) |
| 异步校验器一直 pending | Observable 未正确 complete,或未处理错误 | 确保异步校验返回的 Observable 会 emit 并 complete |
| 表单重置后校验状态未清空 | 只用了 reset() 未清 touched/dirty 状态 | form.reset(initialValue) 会同时清空这些状态,确认调用方式正确 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| 表单简介 | 官方 |
| 响应式表单 | Reactive |
| 验证器 | 校验 |
| Typed Forms | 类型安全 |
| 动态表单 | FormArray 场景 |
相关文章
生态集成
Angular 官方维护路由、HTTP、表单、动画等;UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方/半官方库、常用第三方库、与 Vue/React 生态对照、选型建议与常见坑。
Signal 基础
Signals 是 Angular 16 引入的响应式原语:可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新,涵盖 API、input signal、与变更检测的关系、最佳实践与常见坑。
RxJS 互操作
Angular 内置 RxJS 处理异步流(HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等)。Signal 适合同步/短链路状态;长生命周期流仍常用 Observable。涵盖 async 管道、toSignal/toObservable、常用操作符、最佳实践与常见坑。
状态管理
Angular 没有强制单一状态库:服务 + DI、Signal、RxJS 即可覆盖多数场景;大型应用可选用 NgRx。涵盖选型、服务作为状态容器、跨组件共享、NgRx 与 SignalStore、常见坑。
HTTP 与数据
Angular 通过 HttpClient(@angular/common/http)发起 HTTP 请求,返回 RxJS Observable。见 RxJS 互操作、依赖注入。
变更检测
Angular 在数据变化后更新 DOM 的过程称为 变更检测(Change Detection)。理解默认策略、Zone.js 机制、OnPush 与 Signal 协同,有助于性能调优。运行时技巧见 runtime。