Angular Signal Forms 校验实战:用 Schema 函数为表单添加 required / email 字段校验
本文是 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 函数具备两个直观优势:
- 单一声明点:所有字段的校验规则集中在一段结构化代码里,便于阅读、审查与维护;
- 声明式、自动执行:校验器不需要手动触发,当用户与表单交互、字段值变化时,框架会自动运行绑定在该字段上的校验规则,并把校验结果反映到字段状态上。
本教程的练习文件 app.ts 给出了待完善的起点:已经通过 signal<LoginData> 定义了包含 email、password、rememberMe 三个字段的数据模型,并将该 signal 传给了 loginForm = form(this.loginModel)。后续所有步骤就是在此代码基础上,一步步加入校验逻辑;最终参考答案可在 answer/src/app/app.ts 中查看。
第一步:从 @angular/forms/signals 导入内置校验器
Signal Forms 的内置校验器与 form、FormField 一起从 @angular/forms/signals 导出。本步需要引入两个校验器 required 与 email:
import {form, FormField, required, email} from '@angular/forms/signals';
其中 FormField 是用于在模板中连接表单字段的指令,在前面的 连接表单模板 步骤中已经引入过。
这两个校验器的公开 API 从 Angular 22 开始正式对外提供(源码中标记为 @publicApi 22.0),它们的真实实现位于 packages/forms/signals/src/api/rules/validation/ 目录下,分别是 required.ts 与 email.ts。同目录下还包含 min、max、min_length、max_length、pattern、min_date、max_date 等其他内置校验规则文件,说明这套校验体系是成族提供的。
第二步:给 form() 添加 Schema 函数作为第二个参数
将 form() 的调用更新为携带第二个参数——schema 函数。schema 函数接收一个 fieldPath 参数,借助它可以访问到表单的每一个字段:
loginForm = form(this.loginModel, (fieldPath) => {
// Validators will go here
});
fieldPath 是访问字段的“类型安全句柄”:它能精确反映数据模型的结构与字段类型。比如本例中 loginModel 里 email 是字符串、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 选项的作用是为用户定制可读的错误提示:当对应校验失败时,这段文字会成为展示给用户的错误消息。
一个容易忽略的细节:required 与 email 的职责有明确分层。从 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.ts 与 email.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错误是RequiredValidationError(kind = 'required',见 validation_errors.ts),邮箱格式错误是EmailValidationError(kind = 'email',见 validation_errors.ts)。通过kind你可以精确判断失败原因,比如区分“没填”还是“填错格式”,从而展示不同的提示;message:就是你通过{message: '...'}配置的自定义文案。不传message时,框架会给出默认文案。
仓库还以相同模式内置了 min、max、minDate、maxDate、minLength、maxLength、pattern 等错误类型(参见 validation_errors.ts),因此字段的 errors() 返回结果既可以用 e.kind 做分支判断,也可以借助 instanceof NgValidationError 与 switch 对标准错误做类型收窄。
校验规则自动运行:你需要知道的两个触发要点
正如教程收尾所述,这些校验器在用户与表单交互时会自动运行:当用户输入值变化,schema 函数中绑定到该字段路径的校验器便会被重新评估;一旦校验失败,字段的 errors() 状态(即“字段状态中的错误”)就会反映这一结果。
在这个阶段,你有两点值得留意:
- 校验是基于字段值的响应式更新,而不是一次性静态检查。
validate系列规则(见 validate.ts)绑定在字段路径上,值变化即重新求值; - “空值”与“必填”是两套独立语义:
required()用自己的isEmpty判定“是否为空”;而email()等格式类校验器会主动跳过空值。因此不要试图用email()代替required()做必填校验。
将校验失败的错误展示到页面上,属于教程的下一步“显示校验错误”的内容,其讲解位于 4-display-errors。
进阶:error 与 when——当内置默认行为不够用时
除 message 外,required 与 email 的配置对象还支持另外两个选项,这在 required.ts 与 email.ts 的类型声明中均有体现:
error:用自定义校验错误替换默认错误。它既可以是一个错误对象,也可以是一个接收字段上下文FieldContext并返回错误(或错误数组)的函数,适合需要携带额外业务信息的场景;when:一个接收FieldContext并返回布尔值的条件函数。required()默认认为该字段“总是必填”,而when允许把它变成条件必填——例如“仅当勾选了某个开关时才要求填写”,条件不成立时校验器直接放行(required.ts 的实现里,when先决定REQUIRED_MEMO的值,再决定是否执行空值检查)。email()同样尊重when(email.ts 会优先判断when,不满足则立即返回通过)。
说明:
when与error的具体签名依赖FieldContext类型,属于较进阶的用法;本教程步骤只用到message,但理解这两个选项有助于你推断内置校验器整体可配置能力。
小结
本步骤演示了 Signal Forms 中声明式校验的完整链路:从 @angular/forms/signals 导入 required、email 内置校验器 → 在 form(model, schemaFn) 的第二个参数中书写 schema 函数 → 通过 fieldPath.email / fieldPath.password 精确、类型安全地定位字段 → 用配置对象的 message 提供用户友好文案。校验规则在用户交互时自动执行,失败后错误的 kind 与 message 会同步到字段状态,供模板读取。
完成校验只是表单健壮性的第一步,接下来的 显示校验错误 步骤将带你学习如何在模板中把这些错误渲染出来。若想回顾数据模型如何搭建、模板如何连接,可回看本系列的 1-set-up-form-model 与 2-connect-form-template。
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 StartedRust0627
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