2.7 表单与双向数据流


文档摘要

2.7 表单与双向数据流 Angular 表单有两套体系:模板驱动(ngModel 指令为核心)与响应式(FormControl/FormGroup 核心)。变更检测视角下,表单是密度最高的变更源——每次按键都产生事件、修改控件状态、并等待一轮检查同步视图。两套体系的本质差异在于:表单状态这一"数据"放在模板里还是放在类里。 路由换了子树,表单则在子树内制造每秒可能数十次的细粒度变更。 本节要拿下什么 路由把用户带到页面,表单把用户的话收回来——本节要拿下双向数据流的两种管控体系: 分别用两套体系实现同一表单,说出各自的适用边界。 解释 FormControl 的 valueChanges 与 statusChanges 两个流与变更检测的配合。

2.7 表单与双向数据流

Angular 表单有两套体系:模板驱动(ngModel 指令为核心)与响应式(FormControl/FormGroup 核心)。变更检测视角下,表单是密度最高的变更源——每次按键都产生事件、修改控件状态、并等待一轮检查同步视图。两套体系的本质差异在于:表单状态这一"数据"放在模板里还是放在类里

路由换了子树,表单则在子树内制造每秒可能数十次的细粒度变更。

本节要拿下什么

路由把用户带到页面,表单把用户的话收回来——本节要拿下双向数据流的两种管控体系:

  1. 分别用两套体系实现同一表单,说出各自的适用边界。
  2. 解释 FormControl 的 valueChanges 与 statusChanges 两个流与变更检测的配合。
  3. 编写自定义同步校验器与异步校验器,说明异步校验为何要防抖。
  4. 处理"表单数据变了界面没动"与"界面动得太频繁"两类典型问题。

一、两套体系的同一张表

需求:收货地址表单,收件人必填、手机号格式校验、城市必选。

// 响应式表单:表单模型显式建在类里 import { Component } from '@angular/core'; import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-address-form', imports: [ReactiveFormsModule], template: ` <form [formGroup]="form" (ngSubmit)="save()"> <label>收件人 <input formControlName="name"></label> <label>手机号 <input formControlName="phone"></label> <label>城市 <select formControlName="city"> <option *ngFor="let c of cities" [value]="c">{{ c }}</option> </select> </label> <!-- 校验失败才显示,dirty/touched 避免一进页面就报错 --> <p *ngIf="form.controls.phone.invalid && form.controls.phone.touched" class="err"> 手机号格式不正确 </p> <button [disabled]="form.invalid">保存</button> </form> ` }) export class AddressFormComponent { private fb = inject(FormBuilder); cities = ['上海', '北京', '深圳']; form = this.fb.group({ name: ['', Validators.required], phone: ['', Validators.pattern(/^1\d{10}$/)], city: ['', Validators.required] }); save() { console.log(this.form.value); } }

模板驱动版本则把模型放在模板:[(ngModel)]="addr.phone"required 属性,类里只有一个 addr 对象。选择标准:动态表单(字段运行时生成)、校验逻辑复杂、需要精细监听——用响应式;简单小表单、快速原型——模板驱动够用。团队项目里我建议统一响应式,表单模型可测试性是决定性理由(第 3 章测试一节呼应)。

二、控件状态流与变更检测

FormControl 不只是容器,它自带两个可观察流:

ngOnInit() { this.form.controls.phone.valueChanges.subscribe(v => { console.log('每次按键后都会到这里', v); // 订阅回调在事件里执行;若修改组件字段,随后那轮检查会把变化带上面 }); this.form.statusChanges.subscribe(s => { console.log('有效状态翻转', s); // VALID 与 INVALID 之间切换时发射 }); }

每一次按键的完整链路:input 事件 → 控件值更新 → valueChanges 发射 → 订阅者执行 → 事件结束、检查启动 → 表单相关绑定重算(校验提示、按钮禁用态同步)。表单没做什么特殊魔法,它只是把"用户输入"规范地翻译成"数据变更 + 事件",剩下的仍是主线流程。

⚠️ 高频坑:在 valueChanges 订阅里同步调用 patchValue 改回值(例如格式化手机号),会形成"改值→再发射→再改值"的递归,必须配合原子更新选项(emitEvent 置否)斩断回路:this.form.controls.phone.setValue(formatted, { emitEvent: false })

三、自定义校验器

import { AbstractControl, ValidationErrors } from '@angular/forms'; // 同步校验器:纯函数,返回错误对象或 null export function citySupported(control: AbstractControl): ValidationErrors | null { const supported = ['上海', '北京', '深圳']; return supported.includes(control.value) ? null : { city: { value: control.value } }; } // 异步校验器:返回可观察,典型场景是查重(单号是否已存在) export function orderNoUnique(api: { exists(no: string): boolean }): ValidationErrors | null { return (control: AbstractControl): ValidationErrors | null => { const v = control.value as string; return api.exists(v) ? { unique: true } : null; }; }

异步校验器(真实项目中返回可观察对象的版本)每次值变化都可能触发请求,必须配防抖——手法与 2.4 节搜索框完全一致,这里不再重复。校验器写成纯函数的另一个收益:单元测试不需要任何 Angular 测试设施,直接断言返回值。

图:一次按键在表单体系里的旅程

图:一次按键在表单体系里的旅程

四、案例:动态字段表单

背景:审批流配置页,字段随流程节点不同而不同(有的节点要发票号,有的要预算码),字段配置来自接口。
操作:响应式体系下运行时增删控件:

applySchema(fields: { key: string; required: boolean }[]) { const group: Record<string, FormControl> = {}; for (const f of fields) { group[f.key] = new FormControl('', f.required ? Validators.required : []); } this.form = this.fb.group(group); // 整体重建,模板经 [formGroup] 自动跟随 }

结果:接口返回新字段列表后表单结构即时切换,校验与提交逻辑无需改动。
解读:动态能力是"表单模型在类里"的直接红利——模板驱动版本要配模板引用与嵌套 ngModel,动态化痛苦得多。
变式:字段需要局部增删而非整体重建时,用 addControl/removeControl 增量操作,避免整表值被清。

本节要点回顾

  • 两套体系:模型在类(响应式)还是模板(ngModel);动态与可测优先选响应式。
  • 状态流:valueChanges/statusChanges 把输入翻译成数据变更,随后仍走标准检查流程。
  • 校验器:纯函数同步校验为主,异步校验必配防抖。
  • 递归坑:订阅里改值要 emitEvent 置否,否则死循环。
  • 主线上的一环:表单是最高频变更源,"每键一轮检查"决定了它的优化方向永远是"降低每轮成本"。

下一节讲服务器数据入口:HttpClient。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U