FORMA

表单

Angular 提供 响应式表单(Reactive Forms)模板驱动表单(Template-driven)。复杂表单推荐 Reactive Forms(模型在 TypeScript 中,易测试)。见 组件与模板

核心概念

两种表单方案的本质区别在于**「表单模型的定义位置」**:

  • Reactive Forms:在组件类中用 FormControl / FormGroup / FormArray 显式构建表单模型,模板只负责绑定 formControlName,逻辑集中、易单测、类型可推断。
  • Template-driven:表单模型隐式地由模板中的 [(ngModel)] 生成,写法更接近传统 HTML 表单,适合简单场景,但复杂校验和动态字段处理起来不如 Reactive 直观。

响应式表单

ts
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:动态字段列表

ts
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);
}
html
<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

自定义校验器

ts
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"

html
<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') 的类型丢失:

ts
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 请求:

ts
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(数组本身可变时用索引)
异步校验器一直 pendingObservable 未正确 complete,或未处理错误确保异步校验返回的 Observable 会 emit 并 complete
表单重置后校验状态未清空只用了 reset() 未清 touched/dirty 状态form.reset(initialValue) 会同时清空这些状态,确认调用方式正确

延伸阅读

参考文献

以下链接在编写时均可正常访问:

资料说明
表单简介官方
响应式表单Reactive
验证器校验
Typed Forms类型安全
动态表单FormArray 场景

Series

angular

7 / 14