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字段指令,以及revalidateLogic与onDynamic动态校验策略的组合用法,并了解每一步校验失败时如何阻止进入下一步。读完本文后,你可以直接复刻出具备"分步校验 + 单次最终提交"能力的向导表单。
示例概览:一个最小的两步向导
示例项目位于 examples/angular/multi-step-wizard,使用 Angular CLI 生成(package.json中@angular/cli为^21.2.12),核心依赖为@tanstack/angular-form: ^1.33.5与zod: ^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 serve、ng build、ng test等),真正的技术核心全部在src/app/的五个 TypeScript 组件中,下文将以此为据展开。
第一步:用 formOptions 定义共享表单配置
shared-form.ts 是向导表单的"单一事实来源",它做了两件事:
- 用 zod 定义了两个步骤各自的校验 schema(注意两处的
name长度要求不同:Step1 至少 2 个字符,Step2 至少 3 个字符,用于演示"每步独立校验"); - 用
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)isSubmitting用injectStore从表单 store 中选出,作为提交按钮的禁用依据。
第三步:用 TanStackFormGroup 实现"每步独立提交校验"
TanStackFormGroup是本示例的灵魂——它把整个表单的某个子路径(如step1、step2)提升为一个"组",组拥有自己的校验器、错误地图(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拿到同一个FormApi(withForm.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)结构相同,唯一区别是:
- 校验器换成
step2Schema(name至少 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 会直接渲染为红色提示。
完整工作流串联
把四个文件串起来,向导的运行流程如下:
- 初始状态:根组件
step = signal(0),只渲染app-step1; - Step1 提交:用户点击 Submit,
group.api.handleSubmit()触发——TanStackFormGroup的onDynamic: step1Schema校验step1.name:- 失败 → 触发
onGroupSubmitInvalid,步骤不切换,错误显示在字段下方; - 通过 → 触发
onGroupSubmit,stepChange.emit(1),根组件切到app-step2;
- 失败 → 触发
- Step2 提交:同样先由
step2Schema校验step2.name:- 失败 → 停留本步;
- 通过 →
withForm.form.handleSubmit()调用全局handleSubmit,触发全局onDynamic(对{step1, step2}整体再校验一次)→ 全部通过后执行onSubmit,alert输出完整 JSON;
- 随时返回: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 test(ng test,基于 Karma)。
延伸阅读
- 若想深入了解表单分组 API 的类型与状态结构,可阅读 FormGroupApi 文档 与 FormGroupState 接口文档;
- 全局表单的完整选项(
onDynamic、onSubmit、validationLogic等)见 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),仅供参考