首页
/ Angular Signal Forms 实战指南:基于 Signal 的数据驱动表单实现与验证

Angular Signal Forms 实战指南:基于 Signal 的数据驱动表单实现与验证

2026-09-07 10:42:47作者:蔡怀权

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 指令以及 requiredemail 等校验函数。其中:

创建你的第一个 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 输入类型,同时它还会在合适的时机同步 requireddisabledreadonly 等字段状态到 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 输入而来,还是由业务逻辑写入,最终都收敛到同一个模型上。

完整示例:登录表单

官方文档提供了可直接运行的登录表单示例。组件逻辑与模板实现分别见:

示例中还演示了一个 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() 返回校验错误数组,每一项包含 kindmessage 属性

借助 errors()kind 字段,你可以在模板中对不同类型的错误做差异化展示(例如区分 requiredemail),而不是只能依赖整条消息字符串。

完整示例:带校验的登录表单

官方示例 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 的三个关键机制:

  1. 字段树form() 返回的根对象与嵌套字段都实现自 src/field/node.ts 中的节点逻辑,配合 src/field/state.ts(状态派生)、src/field/validation.ts(校验执行)与 src/field/debounce.ts(防抖)等模块,共同撑起「节点即可调用又可点号访问」的统一 API。

  2. 指令体系src/directive 下按能力拆分了 form_field.ts(面向原生控件)、control_cva.ts(ControlValueAccessor)、control_custom.ts(自定义控件)、select.ts(下拉选择)等实现,这也是文档中「所有标准 HTML 输入类型都能直接用」的底层保证。

  3. 类型安全src/api 中的 types.tsstructure.ts 等文件定义了 FieldTree 的类型系统——由于字段树由模型类型推导而来,loginForm.email 这类访问在编译期就能获得字段级类型提示。

仓库中还提供了更多进阶场景的对照示例(位于 adev/src/content/examples/signal-forms/src):包括与传统响应式表单的 comparison 对比,以及 Signal Forms 与经典 FormControl/FormGroupcompat 集成示例。若你的项目正在逐步从模板驱动/响应式表单迁移,这些例子是很好的过渡参考。

延伸学习

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 成为模型的载体,其余交给框架自动同步。

登录后查看全文
热门项目推荐
相关项目推荐