Angular Signal Forms 表单提交实战:用 submit() 处理登录表单的异步提交
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
导读
本指南讲解 Angular Signal Forms(@angular/forms/signals)教程「Add form submission」中的核心技术:如何借助submit()函数在表单有效时才执行异步提交逻辑,并让表单自动管理提交中(submitting)状态、按需禁用提交按钮。这是 Signal Forms 完整登录表单教程的第 5 步,承接表单建模(form())、模板连接(FormField)、校验规则与错误展示等前置环节。读完本文,你将掌握submit()的完整调用方式、底层执行语义、onSubmit处理器的标准写法,以及如何用loginForm().invalid()动态禁用按钮,最终交付一个可用的登录表单实现。
系列教程与本步所处位置
在 Angular 官方仓库的 adev 教程站点中,Signal Forms 教程按步骤组织在 adev/src/content/tutorials/signal-forms 目录下,本步对应的原始文档为 steps/5-add-submission/README.md。其前置环节依次为:
- 第 1 步「Set up form model」:用
form()基于signal数据模型建立表单; - 第 2 步「Connect form template」:用
FormField指令把输入控件绑定到字段; - 第 3 步「Add validation」:通过
required、email等规则声明校验; - 第 4 步「Display errors」:读取
errors()与touched()展示错误信息。
本步(第 5 步)解决的是最后一块拼图——提交。每一步目录中都包含src/(练习起点)与answer/(参考答案)两个可运行工程,其中本步练习起点见 src/app/app.ts,参考答案见 answer/src/app/app.ts 与 answer/src/app/app.html。
学习目标
完成本步后你将能够:
- 从
@angular/forms/signals导入submit函数; - 在组件类中编写
onSubmit()提交处理方法; - 让异步提交逻辑仅在表单校验通过时执行;
- 依据表单状态动态禁用提交按钮,避免无效提交。
第一步:导入 submit 函数
submit与系列前几步使用的form、FormField、required、email一样,统一从@angular/forms/signals导出。扩展既有导入语句:
import {form, FormField, required, email, submit} from '@angular/forms/signals';作为佐证,submit函数定义在表单信号包的公共 API 中,其实现在 packages/forms/signals/src/api/structure.ts,并在源码注释中被标注为@publicApi 22.0,属于信号表单的公开稳定接口。
第二步:编写 onSubmit 提交处理方法
在组件类中新增onSubmit()方法,把表单与异步回调交给submit():
onSubmit(event: Event) { event.preventDefault(); submit(this.loginForm, async () => { const credentials = this.loginModel(); console.log('Logging in with:', credentials); // Add your login logic here }); }submit() 的核心行为
submit()会仅在表单有效时运行传入的异步回调,并自动管理表单的提交状态。逐行拆解上面的写法:
event.preventDefault():阻止浏览器对原生<form>的默认整页刷新行为,提交改由 Angular 事件绑定接管;submit(this.loginForm, async () => {...}):传入表单树this.loginForm与异步动作action。submit()的函数签名支持两种传参方式——直接传动作回调,或传一个包含action等字段的配置对象(见 packages/forms/signals/src/api/structure.ts);this.loginModel():在回调内部通过信号读取最新表单值。由于 Signal Forms 的数据模型本身就是组件里的signal,组件类中始终可以同步拿到提交时的真实值。
值得强调的是,submit()的返回类型是Promise<boolean>:异步动作执行完毕、且未返回校验错误时解析为true,否则为false。这一返回值可用于后续扩展“提交成功提示/失败重试”等交互逻辑。
表单无效时不会误触发
如果表单存在校验错误,submit()不会执行动作回调,而是会调用onInvalid回调(若配置)。因此上面代码中的console.log与登录逻辑,天然只会出现在“表单有效”的场景中,无需手动编写if (this.loginForm().valid())之类的分支判断。
第三步:在模板中绑定提交事件
在组件模板中,把onSubmit()绑定到<form>的 submit 事件:
<form (submit)="onSubmit($event)"></form>表单内由<button type="submit">触发的提交会冒泡为 form 的submit事件,事件对象作为$event传入处理器,供其调用preventDefault()。
参考完整模板见 answer/src/app/app.html:表单内包含三个分别通过[formField]="loginForm.email"、[formField]="loginForm.password"、[formField]="loginForm.rememberMe"连接的字段,以及错误展示区块与提交按钮。
第四步:根据表单状态禁用提交按钮
将提交按钮的disabled绑定到表单的invalid()状态:
<button type="submit" [disabled]="loginForm().invalid()">Log in</button>loginForm()是组件中由form()创建的表单信号,读取后返回字段代理对象;invalid()是其暴露的派生信号,表单存在校验错误时返回true。上述写法保证了:
- 任一必填项为空或邮箱格式非法时,按钮置灰,无法点击;
- 校验一旦全部通过,按钮自动恢复可点击。
这从交互层面进一步杜绝了无效提交——即使绕过按钮直接触发 submit,submit()在逻辑层面也会做同样的有效性检查,形成“UI 禁用 + 逻辑校验”双保险。
深入原理:submit() 在源码中究竟做了什么
阅读 packages/forms/signals/src/api/structure.ts 中的submit()实现,可以确认它的执行语义如下:
- 防重入保护:若表单当前已在提交中(
submitState.submitting为真),立即返回false,避免回调被重复触发(见 L454-L456); - 动作校验:若最终没有可用的
action,抛出MISSING_SUBMIT_ACTION运行时错误。这意味着你既可以在创建表单时通过form(model, {...})配置submission.action,也可以在调用submit()时按上面的示例传入动作(见 L468-L475); - 自动标记 touched:提交前会调用
markAsTouched(),把整个字段树的 touched 状态标记为 true(见 L477)。这解释了第 4 步教程中“字段被触碰后才展示错误”的 UI 惯例——用户点击登录即代表“触碰”全部字段,错误信息随之显现; - 有效性判断:默认策略下,只有当表单不处于 invalid 状态时才运行动作;
ignoreValidators配置为'all'可跳过校验直接执行,'none'则要求表单完全 valid(见 shouldRunAction)。注意默认行为会忽略处于 pending(异步校验中)的校验器,避免“一边提交一边校验未完成”的竞态; - 提交状态自动启停:进入动作前把
selfSubmitting置为 true,动作无论成功、失败或抛错,都会在finally中复位为 false(见 L483-L495); - 服务端校验错误回填:动作回调可返回
ValidationError或错误数组,submit()会通过setSubmissionErrors把错误回填到对应字段的submissionErrors,可被错误展示逻辑读取(见 L533-L554)。
FieldSubmitState:submitting 状态从何而来
“提交中”状态并非魔法,其载体是 packages/forms/signals/src/field/submit.ts 中的FieldSubmitState类:
selfSubmitting:记录该字段是否正被直接提交;submitting:一个computed信号,值为自身提交中或任一父级字段提交中(见 submit.ts),因此整棵表单树的节点都能感知到根表单进入提交中;submissionErrors:存放最近一次提交带回的校验错误,并会随表单值变化自动清空。
每个字段节点在初始化时挂载FieldSubmitState(见 packages/forms/signals/src/field/node.ts),并通过submitting信号对外暴露。这意味着你还可以进一步扩展:比如用loginForm().submitting()在按钮上展示“登录中…”加载文案或转圈动画。
完整登录表单:把每一步串起来
结合本系列第 1~4 步的内容,完整的组件与模板如下。
组件逻辑(见 answer/src/app/app.ts):
import {Component, signal} from '@angular/core'; import {email, form, FormField, required, submit} from '@angular/forms/signals'; interface LoginData { email: string; password: string; rememberMe: boolean; } @Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [FormField], }) export class App { loginModel = signal<LoginData>({ email: '', password: '', rememberMe: false, }); 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'}); }); onSubmit(event: Event) { event.preventDefault(); submit(this.loginForm, async () => { const credentials = this.loginModel(); console.log('Logging in with:', credentials); // Add your login logic here }); } }模板片段(完整版见 answer/src/app/app.html):
<form (submit)="onSubmit($event)"> <div> <label> Email <input type="email" [formField]="loginForm.email" /> </label> @if (loginForm.email().invalid() && loginForm.email().touched()) { <div class="error"> @for (error of loginForm.email().errors(); track error.kind) { <span>{{ error.message }}</span> } </div> } </div> <!-- Password 与 Remember me 字段的绑定方式以此类推 --> <button type="submit" [disabled]="loginForm().invalid()">Log in</button> </form>登录动作回调中的// Add your login logic here位置即为接入真实 HTTP 登录服务、调用认证 API 或存储会话信息的落点。若异步服务返回字段级错误(如“邮箱不存在”),可将其整理为校验错误对象从回调返回,submit()会将其回填给对应字段供 UI 展示。
实践建议与常见注意事项
- 务必调用
event.preventDefault():若遗漏,浏览器默认行为会导致页面整页刷新,异步提交逻辑将随之丢失; - 利用返回值做成功分流:
submit()返回的Promise<boolean>可在await后决定是否跳转路由或弹出成功提示;由于submit()带有防重入保护,连续点击“提交”不会重复发起请求,可不额外加锁; - 提交按钮的 disabled 只是 UI 层:即使按钮未被禁用,表单无效时
submit()也会拒绝执行动作,两层校验相互兜底; - 服务端错误与本地校验统一展示:动作回调返回的错误会进入
submissionErrors,与本地required/email规则产生的错误共同构成errors(),展示逻辑无需区分来源; - 运行时错误提示:若既未在创建表单时配置
submission动作,也未向submit()传入动作,会抛出MISSING_SUBMIT_ACTION错误并给出明确提示(见 packages/forms/signals/src/api/structure.ts)。
结语
至此,你已用 Signal Forms 从零构建出一个完整的登录表单:信号驱动的数据模型、声明式校验规则、错误展示,以及本步加入的“有效才提交、提交中自动管理状态、按钮随状态禁用”的提交闭环。如果你希望进一步了解表单提交选项(FormSubmitOptions的action/onInvalid/ignoreValidators等完整配置,见 packages/forms/signals/src/api/types.ts)、校验器在提交失败后的执行时序,或阅读该功能对应的单元测试用例 submit.spec.ts 加深理解,可以继续探索本系列的第 6 步 6-next-steps 与仓库源码。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考