Angular Signal Forms 实战指南:基于 Signal 的数据驱动表单实现与验证
Signal Forms 是 Angular 原生表单家族中面向响应式编程的新一代方案,它使用 Angular Signals 管理表单状态,实现数据模型与 UI 之间的自动双向同步。本文以 Angular 仓库中的官方指南 signal-forms.md 为骨架,并结合 @angular/forms/signals 的源码与完整示例进行深度展开。读完本文,你将掌握 Signal Forms 从创建模型、绑定控件、读取状态到内置校验与错误展示的完整链路,并能直接上手写出可运行的登录表单。
什么是 Signal Forms
Signal Forms 的核心思路是:用 Signal 直接承载表单数据模型,并在其上构建一个与模型结构一一对应的「字段树」(FieldTree)。与传统 FormGroup/FormControl 相比,你不再需要重复声明校验器与值容器,模板与数据模型之间的同步由信号机制自动完成。
用一句话概括其工作方式:
Signal Forms 使用 Angular Signals 管理表单状态,在数据模型与 UI 之间提供自动同步。
从源码结构上看,这一整套能力被组织在 packages/forms/signals 目录下,并通过 public_api.ts 对外导出 form()、FormField 指令以及 required、email 等校验函数。其中:
- src/directive/form_field.ts 实现了模板中使用的
[formField]指令(面向原生控件); - src/field/node.ts 定义了
FieldTree节点的核心实现; - src/schema/schema.ts 定义了用于配置校验规则的模式函数(schema function)机制。
创建你的第一个 Signal Form
Signal Forms 的接入遵循一条非常固定的五步链路:定义模型信号 → 用 form() 建立字段树 → 用 [formField] 绑定输入 → 读取状态 → 程序化更新值。
第 1 步:用 signal() 创建表单数据模型
每个表单都始于一个持有数据模型的 Signal:
interface LoginData {
email: string;
password: string;
}
const loginModel = signal<LoginData>({
email: '',
password: '',
});
这里的 loginModel 就是整个表单的单一数据源。Signal Forms 的设计意图在于:你只维护这一份模型,其余的表单状态均由框架派生。
第 2 步:将模型传给 form(),得到 FieldTree
把模型信号传入 form() 函数,即可得到一棵字段树——一个镜像模型结构、支持点号(dot notation)访问的对象结构:
const loginForm = form(loginModel);
loginForm; // 是 FieldTree(根节点)
loginForm.email; // 同样是一个 FieldTree 节点
注意:根表单对象和它的每个嵌套属性都是 FieldTree 节点,因此「整表」与「单字段」共享完全一致的状态读取 API。
第 3 步:用 [formField] 指令绑定 HTML 输入
在模板中使用 [formField] 指令建立双向绑定:
<input type="email" [formField]="loginForm.email" />
<input type="password" [formField]="loginForm.password" />
用户输入等变更会自动更新表单(进而更新底层模型信号)。该指令支持全部标准 HTML 输入类型,同时它还会在合适的时机同步 required、disabled、readonly 等字段状态到 DOM 属性,保证 HTML 自带的约束提示也能正常工作。
第 4 步:通过 FieldTree 的 Signal 读取状态
把任一 FieldTree 节点当作函数调用,即可获得一个包含值、校验状态、交互状态等响应式信号的状态对象:
loginForm(); // 返回整个表单的状态
loginForm.email(); // 返回 email 字段的状态
读取当前值只需调用状态中的 value() 信号:
<!-- 随用户输入自动更新的渲染结果 -->
<p>Form value: {{ loginForm().value() | json }}</p>
<p>Email: {{ loginForm.email().value() }}</p>
// 在组件代码中获取当前值
const currentEmail = loginForm.email().value();
第 5 步:用 set() 程序化更新值
你可以通过任意节点状态上的 value.set() 方法程序化更新值,这一步会同时更新 FieldTree 与底层模型信号:
loginForm.email().value.set('alice@wonderland.com');
赋值后,模型信号自动保持一致:
console.log(loginModel().email); // 'alice@wonderland.com'
这正体现了 Signal Forms「模型即真源」的设计:无论数据是从 UI 输入而来,还是由业务逻辑写入,最终都收敛到同一个模型上。
完整示例:登录表单
官方文档提供了可直接运行的登录表单示例。组件逻辑与模板实现分别见:
- login-simple/app/app.ts:定义
loginModel与loginForm,在onSubmit中直接读取this.loginModel()拿取凭据; - login-simple/app/app.html:绑定输入框,并用
loginForm.email().value()实时展示输入内容。
示例中还演示了一个 Signal Forms 的典型收益:提交时无需像传统模板驱动表单那样逐个提取控件值,this.loginModel() 就是可直接提交的普通对象。
基础用法:各类输入控件的绑定
[formField] 指令与所有标准 HTML 输入类型协作良好,下面是最常见的几种模式。
文本输入
文本输入适用于各类 type 及多行文本域:
<!-- 文本与邮箱 -->
<input type="text" [formField]="form.name" />
<input type="email" [formField]="form.email" />
数字
数字输入会在字符串与数字之间自动转换,因此模型字段可以是 number 类型:
<!-- number - 自动转换为 number 类型 -->
<input type="number" [formField]="form.age" />
日期与时间
日期输入以 YYYY-MM-DD 字符串存储值,时间输入使用 HH:mm 格式:
<!-- 日期和时间 - 以 ISO 格式字符串存储 -->
<input type="date" [formField]="form.eventDate" />
<input type="time" [formField]="form.eventTime" />
如果业务层需要真正的 Date 对象,可以自行转换——将字段值传入 Date() 即可:
const dateObject = new Date(form.eventDate().value());
多行文本
<textarea> 与文本输入行为一致:
<!-- 文本域 -->
<textarea [formField]="form.message" rows="4"></textarea>
复选框
复选框绑定布尔值:
<!-- 单个复选框 -->
<label>
<input type="checkbox" [formField]="form.agreeToTerms" />
I agree to the terms
</label>
多个复选框
多个选项时,为每个选项建立独立的布尔字段:
<label>
<input type="checkbox" [formField]="form.emailNotifications" />
Email notifications
</label>
<label>
<input type="checkbox" [formField]="form.smsNotifications" />
SMS notifications
</label>
单选按钮
单选按钮与复选框类似,但有一个贴心细节:只要多个单选用同一个 [formField] 值,Signal Forms 会自动为它们绑定同一个 name 属性,从而保证浏览器原生的单选互斥行为生效:
<label>
<input type="radio" value="free" [formField]="form.plan" />
Free
</label>
<label>
<input type="radio" value="premium" [formField]="form.plan" />
Premium
</label>
当用户选中某个单选按钮时,表单字段存储该按钮 value 属性的值。例如选择 "Premium" 后,form.plan().value() 即为 "premium"。
下拉选择框
<select> 同时支持静态与动态选项:
<!-- 静态选项 -->
<select [formField]="form.country">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
<!-- 配合 @for 的动态选项 -->
<select [formField]="form.productId">
<option value="">Select a product</option>
@for (product of products; track product.id) {
<option [value]="product.id">{{ product.name }}</option>
}
</select>
注意一个当前版本的限制:[formField] 指令暂不支持多选(<select multiple>),这类场景需要借助其他方案(如自定义控件支持,见源码 src/directive/control_custom.ts)实现。
校验与状态管理
Signal Forms 提供开箱即用的内置校验器。开启校验只需给 form() 传入第二个参数——一个模式函数(schema function):
const loginForm = form(loginModel, (schemaPath) => {
debounce(schemaPath.email, 500);
required(schemaPath.email);
email(schemaPath.email);
});
模式函数接收一个 schema path(模式路径) 参数,它提供到各字段的路径引用,用于逐字段配置校验规则。上面的示例同时演示了 debounce(schemaPath.email, 500) 的用法——它让 email 字段的校验在输入停止 500ms 后才执行,可避免每次击键都触发校验。
常用内置校验器
| 校验器 | 作用 |
|---|---|
required() |
确保字段有值 |
email() |
校验邮箱格式 |
min() / max() |
校验数字范围 |
minLength() / maxLength() |
校验字符串或集合长度 |
pattern() |
用正则表达式校验 |
上述校验函数在 @angular/forms/signals 的公开 API 中导出,与之配套的类型定义集中在 src/api/rules 目录下。
自定义错误消息
每个校验器都接受一个选项对象作为第二个参数,用于自定义错误消息:
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Please enter a valid email address'});
自定义消息会被写入字段的 errors() 状态中,供模板直接渲染。
FieldTree 状态信号
树中的每个节点(包括根表单对象)都暴露同一组响应式状态信号。因为每个节点都是 FieldTree,所以在任意层级监控有效性与交互状态的 API 都是完全一致的——这是 Signal Forms 相比传统表单「根表单一个 API、控件另一个 API」的一大改进。
| 状态 | 说明 |
|---|---|
valid() |
该节点通过全部校验规则时返回 true |
invalid() |
存在校验错误时返回 true |
pending() |
异步校验进行中时返回 true |
touched() |
用户聚焦后又失焦(该字段或其子字段)时返回 true |
dirty() |
值被用户修改过时返回 true |
disabled() |
节点被禁用时返回 true |
readonly() |
节点为只读时返回 true |
errors() |
返回校验错误数组,每一项包含 kind 与 message 属性 |
借助 errors() 的 kind 字段,你可以在模板中对不同类型的错误做差异化展示(例如区分 required 与 email),而不是只能依赖整条消息字符串。
完整示例:带校验的登录表单
官方示例 login-validation 演示了校验接入的完整姿势:
- app.ts:通过模式函数为 email 配置
required+email(含自定义消息),为 password 配置required; - app.html:在用户「接触过且不合法」时(
touched() && invalid())用@for遍历errors()渲染消息列表,例如:
@if (loginForm.email().touched() && loginForm.email().invalid()) {
<ul class="error-list">
@for (error of loginForm.email().errors(); track error) {
<li>{{ error.message }}</li>
}
</ul>
}
这种写法保证了错误提示只在用户真正离开字段后才出现,而不是刚打开页面就铺满红色提示,交互体验更接近直觉。
源码视角:这一切是如何实现的
回到仓库源码层面,可以更清晰地理解 Signal Forms 的三个关键机制:
-
字段树:
form()返回的根对象与嵌套字段都实现自 src/field/node.ts 中的节点逻辑,配合 src/field/state.ts(状态派生)、src/field/validation.ts(校验执行)与 src/field/debounce.ts(防抖)等模块,共同撑起「节点即可调用又可点号访问」的统一 API。 -
指令体系:src/directive 下按能力拆分了
form_field.ts(面向原生控件)、control_cva.ts(ControlValueAccessor)、control_custom.ts(自定义控件)、select.ts(下拉选择)等实现,这也是文档中「所有标准 HTML 输入类型都能直接用」的底层保证。 -
类型安全:src/api 中的
types.ts、structure.ts等文件定义了FieldTree的类型系统——由于字段树由模型类型推导而来,loginForm.email这类访问在编译期就能获得字段级类型提示。
仓库中还提供了更多进阶场景的对照示例(位于 adev/src/content/examples/signal-forms/src):包括与传统响应式表单的 comparison 对比,以及 Signal Forms 与经典 FormControl/FormGroup 的 compat 集成示例。若你的项目正在逐步从模板驱动/响应式表单迁移,这些例子是很好的过渡参考。
延伸学习
Signal Forms 是一套仍在快速演进的体系。若要深入了解其工作机制,官方文档指引了以下进阶主题(对应仓库内更深层的内容组织):
- Overview(Signal Forms 概述与适用场景)——对应
guide/forms/signals/overview入口,可结合 PACKAGE.md 阅读; - Form models(如何用 Signal 创建与管理表单数据)——对应
guide/forms/signals/models; - Field state management(校验状态、交互追踪与字段可见性)——对应
guide/forms/signals/field-state-management,是理解touched/dirty/disabled等状态信号的最佳起点; - Validation(内置校验器、自定义校验规则与异步校验)——对应
guide/forms/signals/validation。
如果你是第一次接触 Signal Forms,建议从本文的登录表单示例入手亲手运行一遍,再逐步深入状态管理与校验主题。核心心法始终只有一句:让 Signal 成为模型的载体,其余交给框架自动同步。
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 StartedRust0625
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