Angular Signal Forms:基于信号的表单体系总览与源码级实践指南
Signal Forms(信号表单)是 Angular 官方 @angular/forms 包内置的一套表单解决方案,它以 Angular Signals 为响应式基础,通过 form() 创建类型安全的字段树(FieldTree),用 [formField] 指令实现输入控件与数据模型之间的自动双向绑定。本文基于仓库文档 adev/src/content/guide/forms/signals/overview.md 展开,并结合 packages/forms/signals/ 目录下的真实源码,带你完整掌握 Signal Forms 的适用场景、接入方式、核心 API 以及底层实现机制。
什么是 Signal Forms
在 Web 应用里构建表单,通常要同时处理多个相互纠缠的问题:追踪字段值、校验用户输入、处理错误状态、以及让 UI 与数据模型保持同步。如果把这些关注点分开管理,就会产生大量样板代码和复杂度。
Signal Forms 建立在信号的响应式基础之上,通过三个核心能力应对这些挑战:
- 自动状态同步——表单数据模型与绑定的表单字段之间自动同步,无需手写
setValue/valueChanges订阅; - 类型安全——表单模式(schema)与 UI 控件到数据模型之间的绑定都具备完整的类型安全;
- 集中式校验——所有校验规则集中定义在一个校验模式函数中,而不是散落在各个控件里。
在仓库中,这套 API 以 @angular/forms/signals 子模块的形式发布,源码位于 packages/forms/signals/。该目录下的 PACKAGE.md 明确说明:它是构建在 Angular Signals 之上的信号版表单 API,是模板驱动表单(template-driven)与响应式表单(reactive forms)之外的替代方案,并且与现有 @angular/forms API 保持互操作。
何时使用 Signal Forms
官方文档给出的选型建议是:
- Signal Forms 最适合从零开始、以信号为核心构建的新应用;
- 如果你维护的是一个使用响应式表单的既有应用,或者你需要生产环境的稳定性保证,响应式表单依然是可靠的选择;
- 如果你从模板驱动表单或响应式表单迁移而来,可以参考专门的对比文档 adev/src/content/guide/forms/signals/comparison.md 与迁移文档 adev/src/content/guide/forms/signals/migration.md。
前置条件
Signal Forms 要求 Angular v21 或更高版本。
就当前仓库而言,根目录 package.json 中的版本号为 22.2.0-next.4(主分支)。值得注意的是,源码中对部分新 API(如 FORM_FIELD 注入令牌、表单控件契约接口)标注了 @publicApi 22.0,说明这些契约是在 22.0 中正式对外暴露的。如果你的项目尚停留在 v21,核心用法(form() + [formField])可用,但涉及自定义控件契约等 22.0 新 API 时应注意版本差异。
接入与导入
Signal Forms 已经包含在 @angular/forms 包内,无需额外安装依赖。从 @angular/forms/signals 导入所需函数和指令即可:
import {form, FormField, required, email} from '@angular/forms/signals';
FormField 指令必须导入到任何需要将表单字段绑定到 HTML 输入控件的组件中:
@Component({
// ...
imports: [FormField],
})
公共 API 入口一览
从 public_api.ts 可以看到,该子模块对外导出的内容包括:
| 导出模块 | 对应源码位置 | 职责 |
|---|---|---|
form() 等断言/结构函数 |
assertions.ts、structure.ts | 构建字段树、类型断言 |
校验规则(required、email、min、max、pattern 等) |
rules/ | 同步/异步校验、禁用、隐藏、元数据规则 |
debounce() |
debounce.ts | 校验防抖 |
transformedValue() |
transformed_value.ts | 值转换 |
FormField / FormRoot 指令 |
form_field.ts、form_root.ts | 双向绑定与表单根节点 |
| WebMCP 支持 | webmcp/ | 表单与 WebMCP 的集成 |
核心使用流程
下面是最小可用路径的完整示例(与文档 Signal Forms essentials 中给出的模式一致,完整可运行示例可参考该文档以及仓库中的示例工程目录 adev/src/content/examples/signal-forms/)。
1. 用 signal() 创建表单模型
每个表单都从一个保存数据模型的信号开始:
interface LoginData {
email: string;
password: string;
}
const loginModel = signal<LoginData>({
email: '',
password: '',
});
2. 用 form() 生成 FieldTree
把表单模型传入 form(),得到一个字段树(FieldTree)——一个与模型形状一致的对象结构,可以按点号访问各字段。根表单对象与它的嵌套属性都是 FieldTree 节点:
const loginForm = form(loginModel);
loginForm; // 是一个 FieldTree
loginForm.email; // 也是一个 FieldTree
3. 用 [formField] 指令绑定输入控件
[formField] 在 HTML 输入控件与字段之间建立双向绑定:
<input type="email" [formField]="loginForm.email" />
<input type="password" [formField]="loginForm.password" />
用户输入会自动更新表单;该指令还会在适当时候同步 required、disabled、readonly 等字段状态。[formField] 兼容所有标准 HTML 输入类型,常见模式包括:
- 文本/邮箱输入:
<input type="text" [formField]="form.name" />、<input type="email" [formField]="form.email" /> - 数字输入:
<input type="number" [formField]="form.age" />,自动在字符串与数字之间转换 - 日期/时间:
<input type="date">存储YYYY-MM-DD字符串,<input type="time">使用HH:mm格式,需要Date对象时可new Date(form.eventDate().value())转换 - 多行文本:
<textarea [formField]="form.message" rows="4"></textarea> - 复选框:绑定布尔值,多选项时为每项创建独立布尔
formField - 单选按钮:同一组 radio 使用相同
[formField],Signal Forms 会自动为它们绑定相同的name属性,选中后字段值为对应 radio 的value属性 - 下拉选择:
<select>支持静态选项与@for动态选项
4. 用 FieldTree 状态信号读取状态
调用任意节点即可取得其状态对象,其中包含值、校验状态与交互状态的响应式信号:
loginForm(); // 整个表单的状态
loginForm.email(); // email 字段的状态
每个节点(包括根节点)都暴露同一组状态信号,API 在树的每一级完全一致:
| 状态信号 | 含义 |
|---|---|
valid() |
该节点通过所有校验规则 |
invalid() |
存在校验错误 |
pending() |
异步校验正在进行中 |
touched() |
用户已聚焦并离开该字段(或任一子字段) |
dirty() |
值已被用户修改 |
disabled() |
节点被禁用 |
readonly() |
节点为只读 |
errors() |
校验错误数组,每项含 kind 与 message 属性 |
<!-- 随用户输入自动更新的值显示 -->
<p>Form value: {{ loginForm().value() | json }}</p>
<p>Email: {{ loginForm.email().value() }}</p>
5. 用 value.set() 编程式更新
任意节点都可以通过 value.set() 更新值,字段树与底层模型信号会同步更新:
loginForm.email().value.set('alice@wonderland.com');
console.log(loginModel().email); // 'alice@wonderland.com'
校验:把规则集中在 schema 函数里
将模式函数作为 form() 的第二个参数传入,即可声明校验规则。模式函数接收一个 schema path 参数,提供指向各字段的路径:
const loginForm = form(loginModel, (schemaPath) => {
debounce(schemaPath.email, 500);
required(schemaPath.email);
email(schemaPath.email);
});
常用内置校验器:required()、email()、min()/max()、minLength()/maxLength()、pattern()。还可通过第二个参数自定义错误信息:
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Please enter a valid email address'});
更多规则细节(自定义规则、异步校验、交叉字段逻辑等)见 Validation 指南 与 Schemas 指南。
源码解析:FormField 指令如何工作
FormField 指令是双向绑定的核心,定义在 form_field.ts 中:
@Directive({
selector: '[formField]',
exportAs: 'formField',
providers: [
{provide: FORM_FIELD, useExisting: FormField},
{provide: NgControl, useFactory: () => inject(FormField).interopNgControl},
{provide: FORM_CONTROL_INTEGRATION, useFactory: () => inject(FORM_FIELD, {self: true})},
],
})
export class FormField<T> { ... }
从源码注释可以确认,该指令承担四项职责:
- 在字段状态的
value与 UI 控件的值之间双向绑定; - 把字段状态中与表单相关的其他状态(
disabled、required等)绑定到 UI 控件上; - 将控件上的相关事件(例如 blur 时标记 touched)中继回字段状态;
- 提供一个实现
NgControl子集接口的“假NgControl”(通过interopNgControl),用于与为响应式表单设计的控件互操作——源码注释明确建议:为信号表单编写的新控件不应依赖这一互操作层。
指令还支持三类宿主控件:原生 HTML input/textarea、实现 FormValueControl 或 FormCheckboxControl 契约的信号表单自定义控件、以及提供 ControlValueAccessor 的组件(仅用于向后兼容响应式表单,官方建议优先使用前两者)。指令还会通过 InputValidityMonitor 与字段节点(field/node.ts)协作管理绑定生命周期,相关行为可在 test/web/form_field.spec.ts 等测试中验证。
自定义控件契约
control.ts 定义了三个公共契约(@publicApi 22.0):
FormUiControl<TValue>(L22):所有表单控件共享的基础契约,声明了大量可选的InputSignal(如errors、disabled、invalid、touched、dirty、required、min/max、pattern等)以及focus()/reset()方法。控件一旦实现这些输入,Field指令就会自动把绑定字段的状态同步进来;FormValueControl<TValue>(L160):编辑任意类型值的控件契约,必须提供value: ModelSignal<TValue>;FormCheckboxControl(L192):编辑布尔型复选框的契约,必须提供checked: ModelSignal<boolean>。
完整的自定义控件编写方法见 Custom controls 指南。
校验规则的实现细节
以 required() 为例,其实现位于 required.ts。除了文档中提到的 message 选项外,源码显示它实际支持更完整的配置项:
message:面向用户的错误文案;error:自定义校验错误(或接收FieldContext返回错误的函数),用于替代默认的ValidationError.required();when:接收FieldContext返回布尔值的函数,用于条件必填——required()会先通过metadata(path, ...)将when的计算结果 memoize 为元数据,只有该值为真且当前值为空时才产生错误。
rules/validation/ 目录下的完整内置规则包括:email.ts、max.ts、max_date.ts、max_length.ts、min.ts、min_date.ts、min_length.ts、pattern.ts、required.ts,以及用于编写自定义规则的基础设施 validate.ts(同步)、validate_async.ts(异步)与 validate_http.ts(HTTP 校验)。规则对应的单元/端到端测试位于 test/node/api/validators/ 目录。
当前限制
阅读文档时应注意以下仓库中明确标注的限制:
- PACKAGE.md 列出了暂不支持的特性:校验防抖(debouncing validation)、动态对象(dynamic objects)、元组(tuples);
- 多值下拉
<select multiple>目前不受[formField]指令支持(见 essentials 指南 的说明)。
在评估是否将 Signal Forms 引入既有生产应用时,应将这些边界纳入考量。
学习路径:配套指南索引
概述文档为深入阅读规划了完整路线,以下是仓库中的对应文档(均位于 adev/src/content/guide/forms/signals/):
| 主题 | 文档 |
|---|---|
| Signal Forms 入门速览 | adev/src/content/introduction/essentials/signal-forms.md |
| 表单模型(创建与管理数据) | models.md |
| 设计表单模型 | designing-your-form-model.md |
| 字段状态管理 | field-state-management.md |
| 校验 | validation.md |
| 自定义控件 | custom-controls.md |
| 与其他表单体系对比 | comparison.md |
| 迁移指南 | migration.md |
| Schema 详解 | schemas.md |
| 测试 | testing.md |
| 异步操作 / 表单提交 / 跨字段逻辑 | async-operations.md、form-submission.md、cross-field-logic.md |
掌握本篇内容后,你将能够:判断 Signal Forms 是否适合你的应用、完成 @angular/forms/signals 的接入配置、用 signal() + form() + [formField] 搭建类型安全的双向绑定表单、通过 schema 函数集中声明校验规则,并理解 FormField 指令、控件契约与校验规则在源码层面的工作机制。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00