TanStack Form 多步骤向导实战:基于 Angular 与 FormGroup 的分步表单实现
2026/9/17 12:39:32 网站建设 项目流程

TanStack Form 多步骤向导实战:基于 Angular 与 FormGroup 的分步表单实现

【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form

导读

本文以仓库内的 examples/angular/multi-step-wizard 示例为主线,讲解如何在 Angular 应用中用 TanStack Form 构建一个"两步走"的多步骤向导(Multi-Step Wizard)表单。你将掌握injectForm全局表单、injectWithForm局部表单注入、TanStackFormGroup分组提交校验、TanStackAppField字段指令,以及revalidateLogiconDynamic动态校验策略的组合用法,并了解每一步校验失败时如何阻止进入下一步。读完本文后,你可以直接复刻出具备"分步校验 + 单次最终提交"能力的向导表单。

示例概览:一个最小的两步向导

示例项目位于 examples/angular/multi-step-wizard,使用 Angular CLI 生成(package.json@angular/cli^21.2.12),核心依赖为@tanstack/angular-form: ^1.33.5zod: ^3.25.76。目录结构如下:

examples/angular/multi-step-wizard/src/app/ ├── app.component.ts # 根组件:切换步骤、持有全局表单 ├── shared-form.ts # 共享的 formOptions 与 zod schema ├── step1.component.ts # 第一步:FormGroup "step1" ├── step2.component.ts # 第二步:FormGroup "step2" └── text-field.component.ts # 可复用字段组件(tanstack-app-field)

整个表单只有两个步骤(Step1 / Step2),每步一个name字段,逻辑非常精简,非常适合作为理解 FormGroup 分步提交的入门样例。注意:该 README 自身只是 Angular CLI 的默认模板说明(ng serveng buildng test等),真正的技术核心全部在src/app/的五个 TypeScript 组件中,下文将以此为据展开。

第一步:用 formOptions 定义共享表单配置

shared-form.ts 是向导表单的"单一事实来源",它做了两件事:

  1. 用 zod 定义了两个步骤各自的校验 schema(注意两处的name长度要求不同:Step1 至少 2 个字符,Step2 至少 3 个字符,用于演示"每步独立校验");
  2. formOptions声明表单的defaultValues,将数据按步骤分区:
import { formOptions } from '@tanstack/angular-form' import { z } from 'zod' export const step1Schema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), }) export const step2Schema = z.object({ name: z.string().min(3, 'Name must be at least 3 characters'), }) export const wizardFormOpts = formOptions({ defaultValues: { step1: { name: '' }, step2: { name: '' }, }, })

关键点在于表单数据不是"当前步骤的数据",而是全部步骤的数据集合{ step1: {...}, step2: {...} }。这样设计使得整个向导最终可以一次性提交完整数据,而不是每步独立提交后再拼接。

第二步:用 injectForm 创建全局表单并绑定动态校验

根组件 app.component.ts 使用injectForm创建全局FormApi实例。injectForm的底层实现位于 packages/angular-form/src/inject-form.ts:它内部new FormApi(opts)创建表单实例,并额外injectStore(api.store, (state) => state.isSubmitting)订阅isSubmitting状态,使其可以在 Angular 变更检测中响应。

form = injectForm({ ...wizardFormOpts, validationLogic: revalidateLogic(), validators: { onDynamic: z.object({ step1: step1Schema, step2: step2Schema, }), }, onSubmit: ({ value }) => { alert(`Form submitted: ${JSON.stringify(value)}`) }, })

这里有两处容易困惑的设计,源码注释解释得很清楚:

  • onDynamic只在调用form.handleSubmit()时才会被触发。当用户点击"下一步"触发的是 FormGroup 的handleSubmit(),它只校验当前步骤的 schema,不会触发这个全局onDynamic;只有走到最后一步、由 Step2 调用整个form.handleSubmit()时,全局的onDynamic(包含两个步骤的 zod 对象)才会对完整数据做最终校验。
  • validationLogic: revalidateLogic()控制"重新校验"策略。查看 packages/form-core/src/ValidationLogic.ts,revalidateLogic的默认参数为{ mode: 'submit', modeAfterSubmission: 'change' }:表单尚未提交时,change事件不触发校验;一旦发生过一次提交,之后在change时就会重新校验。这可以避免用户在向导中还没走到某一步就收到红字报错,同时保证提交过一次后错误能即时更新。

模板部分用@if (step() === 0)/@if (step() === 1)按信号step切换渲染 Step1 / Step2,并通过[form]="form"[step]="step()"[isSubmitting]="isSubmitting()"传入子组件:

step = signal(0) isSubmitting = injectStore(this.form, (state) => state.isSubmitting)

isSubmittinginjectStore从表单 store 中选出,作为提交按钮的禁用依据。

第三步:用 TanStackFormGroup 实现"每步独立提交校验"

TanStackFormGroup是本示例的灵魂——它把整个表单的某个子路径(如step1step2)提升为一个"组",组拥有自己的校验器、错误地图(errorMap/errors)与提交钩子。查看 step1.component.ts:

<ng-container [tanstackFormGroup]="withForm.form" name="step1" [validators]="{ onDynamic: step1Schema }" [onGroupSubmit]="onGroupSubmit" [onGroupSubmitInvalid]="onGroupSubmitInvalid" #group="formGroup" > <form (submit)=" $event.preventDefault(); $event.stopPropagation(); group.api.handleSubmit() "> <app-text-field label="Step 1 Name" tanstack-app-field [tanstackField]="withForm.form" name="step1.name" /> <button type="submit" [disabled]="isSubmitting()">Submit</button> <pre>{{ stringify(group.api.state.meta.errorMap) }}</pre> </form> </ng-container>

组件类侧则通过injectWithForm拿到同一个FormApiwithForm.form),并用stepChange输出事件告知父组件切换步骤:

withForm = injectWithForm({ ...wizardFormOpts }) onGroupSubmit = () => { this.stepChange.emit(this.step() + 1) } onGroupSubmitInvalid = () => { // 组级也能处理校验失败,阻止进入下一步 }

需要明确的关键机制:

  • injectWithForm定义于 packages/angular-form/src/with-form-injectable.ts(经由 packages/angular-form/src/index.ts 导出),它让每个步骤组件都注入同一个表单实例,从而共享同一份数据与状态;
  • 组的校验器[validators]="{ onDynamic: step1Schema }"只作用于本组数据(step1分支);
  • 提交时调用的是group.api.handleSubmit()的提交方法),它只校验当前组;校验通过才回调onGroupSubmit(这里实现"进入下一步");校验失败则回调onGroupSubmitInvalid——这个钩子正是"阻止向导前进到非法步骤"的关键,示例中留空注释说明了其用途;
  • 组同样拥有state.meta.errorMap,与表单、字段一致,可直接在模板中展示。

Step2 组件(step2.component.ts)结构相同,唯一区别是:

  • 校验器换成step2Schemaname至少 3 个字符);
  • 提交成功后不再stepChange.emit(step()+1),而是调用全局this.withForm.form.handleSubmit(),从而触发根组件中定义的全局onDynamic校验(step1 + step2 一起)与onSubmit,完成向导的最终提交;
  • 提供 "Back" 按钮(click)="stepChange.emit(step() - 1)"返回上一步。

第四步:用 TanStackAppField 封装可复用字段

text-field.component.ts 把"标签 + 输入框 + 错误列表"封装成一个可复用字段组件,并通过injectField注入字段 API:

@Component({ selector: 'app-text-field', standalone: true, template: ` <div> <label> <div>{{ label() }}</div> <input [value]="field.api.state.value" (input)="field.api.handleChange($any($event).target.value)" (blur)="field.api.handleBlur()" /> </label> @for (error of field.api.state.meta.errors; track $index) { <div style="color: red">{{ error.message }}</div> } </div> `, }) export class TextFieldComponent { label = input.required<string>() field = injectField<string>() }

其使用方式为:在宿主元素上同时加tanstack-app-field指令与[tanstackField]="withForm.form"name="step1.name"TanStackAppField指令定义于 packages/angular-form/src/app-field.ts,它通过DeepKeys<TParentData>/DeepValue<TParentData, TName>name做类型约束,保证step1.name这类深路径在编译期就是合法的,从而让整个向导表单保持类型安全。字段的错误信息统一取自field.api.state.meta.errors,zod 校验产生的 message 会直接渲染为红色提示。

完整工作流串联

把四个文件串起来,向导的运行流程如下:

  1. 初始状态:根组件step = signal(0),只渲染app-step1
  2. Step1 提交:用户点击 Submit,group.api.handleSubmit()触发——TanStackFormGrouponDynamic: step1Schema校验step1.name
    • 失败 → 触发onGroupSubmitInvalid,步骤不切换,错误显示在字段下方;
    • 通过 → 触发onGroupSubmitstepChange.emit(1),根组件切到app-step2
  3. Step2 提交:同样先由step2Schema校验step2.name
    • 失败 → 停留本步;
    • 通过 →withForm.form.handleSubmit()调用全局handleSubmit,触发全局onDynamic(对{step1, step2}整体再校验一次)→ 全部通过后执行onSubmitalert输出完整 JSON;
  4. 随时返回:Step2 的 Back 按钮发出stepChange.emit(0)回到 Step1,已填数据因共享同一FormApi而完整保留。

运行示例

在 examples/angular/multi-step-wizard 目录下(需先安装依赖,项目为 pnpm workspace 的一部分,见根目录 pnpm-workspace.yaml):

npm install # 或 pnpm install npm start # 等价于 ng cache clean && ng serve

默认开发服务器地址为http://localhost:4200/,修改源码后会自动热重载。生产构建使用npm run build(即ng build),产物输出到dist/目录;单元测试用npm testng test,基于 Karma)。

延伸阅读

  • 若想深入了解表单分组 API 的类型与状态结构,可阅读 FormGroupApi 文档 与 FormGroupState 接口文档;
  • 全局表单的完整选项(onDynamiconSubmitvalidationLogic等)见 FormOptions 文档;
  • revalidateLogic的默认值与运行逻辑见 ValidationLogic 源码 与 revalidateLogic 文档;
  • 其他框架(React / Vue / Solid / Lit / Svelte)均有对应的 multi-step-wizard 示例,可参照对比:例如 examples/react/multi-step-wizard。

【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询