首页
/ Angular Signal Forms 校验实战:用 Schema 函数为表单添加 required / email 字段校验

Angular Signal Forms 校验实战:用 Schema 函数为表单添加 required / email 字段校验

2026-09-07 15:04:16作者:龚格成

本文是 Angular 官方教程 Signal Forms 系列“为表单添加校验”章节的完整技术解析。在 Signal Forms(@angular/forms/signals)中,校验不再像传统 Reactive Forms 那样作为数组挂在控件构造参数上,而是集中声明在一个 schema 函数中,并将其作为 form() 的第二个参数传入。读完本文你将掌握:如何导入内置校验器、如何编写 schema 函数、如何通过 fieldPath 定位字段并为其叠加多条校验规则,以及如何利用 message 等配置产出对用户友好的自定义错误信息。文中所有结论均可在 angular 仓库的 Signal Forms 教程与源码中得到验证。

为什么 Signal Forms 要用 Schema 函数做校验

为表单添加校验是保证用户提交有效数据的关键防线。Signal Forms 的设计思路是:把“表单结构如何组织”和“字段之间如何相互约束”这类声明式逻辑,统一收进一个 schema 函数,由你传入 form() 函数作为第二个参数(第一个参数是驱动表单的数据模型 signal)。

相比模板驱动或 Reactive Forms 中把校验器散落在模板指令与 FormControl 构造参数里的做法,schema 函数具备两个直观优势:

  1. 单一声明点:所有字段的校验规则集中在一段结构化代码里,便于阅读、审查与维护;
  2. 声明式、自动执行:校验器不需要手动触发,当用户与表单交互、字段值变化时,框架会自动运行绑定在该字段上的校验规则,并把校验结果反映到字段状态上。

本教程的练习文件 app.ts 给出了待完善的起点:已经通过 signal<LoginData> 定义了包含 emailpasswordrememberMe 三个字段的数据模型,并将该 signal 传给了 loginForm = form(this.loginModel)。后续所有步骤就是在此代码基础上,一步步加入校验逻辑;最终参考答案可在 answer/src/app/app.ts 中查看。

第一步:从 @angular/forms/signals 导入内置校验器

Signal Forms 的内置校验器与 formFormField 一起从 @angular/forms/signals 导出。本步需要引入两个校验器 requiredemail

import {form, FormField, required, email} from '@angular/forms/signals';

其中 FormField 是用于在模板中连接表单字段的指令,在前面的 连接表单模板 步骤中已经引入过。

这两个校验器的公开 API 从 Angular 22 开始正式对外提供(源码中标记为 @publicApi 22.0),它们的真实实现位于 packages/forms/signals/src/api/rules/validation/ 目录下,分别是 required.tsemail.ts。同目录下还包含 minmaxmin_lengthmax_lengthpatternmin_datemax_date 等其他内置校验规则文件,说明这套校验体系是成族提供的。

第二步:给 form() 添加 Schema 函数作为第二个参数

form() 的调用更新为携带第二个参数——schema 函数。schema 函数接收一个 fieldPath 参数,借助它可以访问到表单的每一个字段:

loginForm = form(this.loginModel, (fieldPath) => {
  // Validators will go here
});

fieldPath 是访问字段的“类型安全句柄”:它能精确反映数据模型的结构与字段类型。比如本例中 loginModelemail 是字符串、password 是字符串,那么在 schema 函数里 fieldPath.email 就是一个字符串字段路径,email() 这类仅限字符串路径的校验器才能绑定上去(email.ts 的注释明确说明其“只能被调用在 string path 上”)。这种类型约束能把“对非字符串字段调用 email 校验”这类低级错误在编译期就拦截下来。

第三步:为 email 字段叠加必填与格式校验

在 schema 函数内部,为 email 字段同时绑定 required()email() 两个校验器。注意这里两个校验器各自带一个配置对象:

loginForm = form(this.loginModel, (fieldPath) => {
  required(fieldPath.email, {message: 'Email is required'});
  email(fieldPath.email, {message: 'Enter a valid email address'});
});

required() 负责拦截空值,email() 负责拦截“非空但格式不正确”的值。message 选项的作用是为用户定制可读的错误提示:当对应校验失败时,这段文字会成为展示给用户的错误消息。

一个容易忽略的细节:requiredemail 的职责有明确分层。从 email.ts 的实现可以看到,email() 在执行格式判断前会先检查当前值是否为空,若值为空则直接放行——也就是说它只对“非空但不合法”的输入报错,空值交给 required() 处理。因此两个校验器叠加使用,恰好互补地覆盖了“必须填写”与“填写了但格式错误”两种场景。

第四步:为 password 字段添加必填校验

同样的模式应用到 password 字段。因为密码字段只需要“非空”约束,绑一个 required() 即可:

loginForm = form(this.loginModel, (fieldPath) => {
  required(fieldPath.email, {message: 'Email is required'});
  email(fieldPath.email, {message: 'Enter a valid email address'});
  required(fieldPath.password, {message: 'Password is required'});
});

至此,完整的 schema 函数如图(与仓库中的 标准答案 一致):

loginForm = form(this.loginModel, (fieldPath) => {
  required(fieldPath.email, {message: 'Email is required'});
  email(fieldPath.email, {message: 'Enter a valid email address'});
  required(fieldPath.password, {message: 'Password is required'});
});

校验器背后的实现原理:required 与 email 到底做了什么

要想把校验用对、用透,值得深入 required.tsemail.ts 的核心实现。

required() 的内部机制包含两层动作:

  • 首先,它会向该字段写入一个 REQUIRED 元数据(REQUIRED_MEMO)。这不仅是校验逻辑,还会给字段打上“必填”标记,供模板渲染(如必填星号、禁用状态)等场景使用;
  • 其次,它通过 validate(path, fn) 注册校验逻辑:只要字段为必填(默认恒为 true,可通过 when 条件动态决定)且 isEmpty(ctx.value()) 判断值为空,就返回一个错误对象。若你在配置里传了 error,则使用自定义错误;否则使用默认的 requiredError() 工厂,并把 message 塞入其中。

email() 的校验标准来自一个移植自 AngularJS 的正则表达式 EMAIL_REGEXP(见 email.ts)。其匹配规则比简单的“是否含 @”严格得多,例如:

  • local-part(@ 前部分)只能包含字母数字与部分标点,不能以点号开头或结尾,且长度不能超过 64 个字符;
  • 域名部分由多个 label 组成,单个 label 不能以横线或点号开头/结尾,长度不超过 63 字符;
  • 整个邮箱地址总长不能超过 254 个字符

只有完全符合该正则的字符串,email() 才判定为通过;否则返回 EmailValidationError

校验失败后字段状态如何变化?validation_errors.ts 可以看到,所有校验错误统一实现 ValidationError 接口,具备两个关键字段:

  • kind:错误类型标识。例如 required 错误是 RequiredValidationErrorkind = 'required',见 validation_errors.ts),邮箱格式错误是 EmailValidationErrorkind = 'email',见 validation_errors.ts)。通过 kind 你可以精确判断失败原因,比如区分“没填”还是“填错格式”,从而展示不同的提示;
  • message:就是你通过 {message: '...'} 配置的自定义文案。不传 message 时,框架会给出默认文案。

仓库还以相同模式内置了 minmaxminDatemaxDateminLengthmaxLengthpattern 等错误类型(参见 validation_errors.ts),因此字段的 errors() 返回结果既可以用 e.kind 做分支判断,也可以借助 instanceof NgValidationError 与 switch 对标准错误做类型收窄。

校验规则自动运行:你需要知道的两个触发要点

正如教程收尾所述,这些校验器在用户与表单交互时会自动运行:当用户输入值变化,schema 函数中绑定到该字段路径的校验器便会被重新评估;一旦校验失败,字段的 errors() 状态(即“字段状态中的错误”)就会反映这一结果。

在这个阶段,你有两点值得留意:

  1. 校验是基于字段值的响应式更新,而不是一次性静态检查。validate 系列规则(见 validate.ts)绑定在字段路径上,值变化即重新求值;
  2. “空值”与“必填”是两套独立语义required() 用自己的 isEmpty 判定“是否为空”;而 email() 等格式类校验器会主动跳过空值。因此不要试图用 email() 代替 required() 做必填校验。

将校验失败的错误展示到页面上,属于教程的下一步“显示校验错误”的内容,其讲解位于 4-display-errors

进阶:error 与 when——当内置默认行为不够用时

message 外,requiredemail 的配置对象还支持另外两个选项,这在 required.tsemail.ts 的类型声明中均有体现:

  • error:用自定义校验错误替换默认错误。它既可以是一个错误对象,也可以是一个接收字段上下文 FieldContext 并返回错误(或错误数组)的函数,适合需要携带额外业务信息的场景;
  • when:一个接收 FieldContext 并返回布尔值的条件函数required() 默认认为该字段“总是必填”,而 when 允许把它变成条件必填——例如“仅当勾选了某个开关时才要求填写”,条件不成立时校验器直接放行(required.ts 的实现里,when 先决定 REQUIRED_MEMO 的值,再决定是否执行空值检查)。email() 同样尊重 whenemail.ts 会优先判断 when,不满足则立即返回通过)。

说明:whenerror 的具体签名依赖 FieldContext 类型,属于较进阶的用法;本教程步骤只用到 message,但理解这两个选项有助于你推断内置校验器整体可配置能力。

小结

本步骤演示了 Signal Forms 中声明式校验的完整链路:从 @angular/forms/signals 导入 requiredemail 内置校验器 → 在 form(model, schemaFn) 的第二个参数中书写 schema 函数 → 通过 fieldPath.email / fieldPath.password 精确、类型安全地定位字段 → 用配置对象的 message 提供用户友好文案。校验规则在用户交互时自动执行,失败后错误的 kindmessage 会同步到字段状态,供模板读取。

完成校验只是表单健壮性的第一步,接下来的 显示校验错误 步骤将带你学习如何在模板中把这些错误渲染出来。若想回顾数据模型如何搭建、模板如何连接,可回看本系列的 1-set-up-form-model2-connect-form-template

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