news 2026/9/17 14:49:10

Solid 多步骤表单构建指南:TanStack Form 的 FormGroup 子表单校验与提交

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Solid 多步骤表单构建指南:TanStack Form 的 FormGroup 子表单校验与提交

Solid 多步骤表单构建指南:TanStack Form 的 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

本指南以 TanStack Form(Solid 适配器)的 Form Group(表单分组)为核心,讲解如何用<form.FormGroup>把大型多步骤表单拆分为独立的子表单,并让每个子表单拥有独立的校验、错误分发与提交语义。读完本文,你将掌握 FormGroup 的声明式用法、组级校验与标准 Schema 组合策略、onDynamic动态校验的推荐姿势,以及group().state.meta聚合状态的使用方法,可直接套用到多步骤向导(Multi-step Wizard)等真实场景。

为什么需要 FormGroup:多步骤表单的拆分痛点

当构建一个包含多个步骤的表单(如分步向导、流程式填写页面)时,每个步骤如果各自维护一个独立的"顶层表单",往往会把表单提交与校验流程复杂化——你需要手工拼接各步骤的数据、分别触发校验、再协调最终提交,逻辑冗长且容易出错。

TanStack Form 为此提供了内建的子表单能力:<form.FormGroup>。它允许你在一个统一的form实例内部声明多个"子表单"(sub-form),每个子表单拥有近似于表单的 API(如deleteFieldinsertFieldValuehandleSubmit),同时又共享父表单的单一状态树,让这类开发变得非常简单。

如下是一个典型的多步骤向导界面,这正是 FormGroup 最典型的应用场景:

基本用法:声明一个 FormGroup

使用 FormGroup 的方式和Field几乎一致:通过createForm(或useAppForm,即createFormHook组合出来的表单)创建form变量,然后引用其FormGroup组件:

const form = createForm(() => ({ defaultValues: { step1: { name: '', }, step2: { age: 0, }, }, })) return ( <form.FormGroup name="step1"> {(group) => ( // `group()` 拥有完整的表单类方法, // 例如 `deleteField`、`insertFieldValue` 等 // ... )} </form.FormGroup> )

这里有几个关键点:

  • name必须指向defaultValues中的一个深层路径(DeepKeys<TParentData>),它确定了该分组在父表单状态树中的挂载位置;
  • 渲染函数(render prop)接收一个访问器(accessor)group,调用group()会得到一个FormGroupApi实例;
  • FormGroupApi同时实现了FormLikeAPIFieldLikeAPI(见 FormGroupApi.ts),所以它既具备表单的方法(如handleSubmit),也具备字段的方法(如setValuevalidate),并且所有操作最终都委托给父表单的FormApi(如deleteField内部调用this.form.deleteField)。

在 Solid 适配器中,form.FormGroupcreateForm返回的扩展 API 的一部分,实现位于 createForm.tsx:extendedApi.FormGroup = (props) => <FormGroup {...props} form={api} />FormGroup组件内部通过createFormGroup创建FormGroupApi,在onMount时调用api.mount()注册到父表单(卸载时清理),并用createComputed在每次渲染前同步最新配置,具体见 createFormGroup.tsx。

与外部状态配合:条件渲染多步骤向导

FormGroup 真正强大之处在于可以和外部状态配合,按步骤条件渲染,实现"每步一个子表单"的向导:

const [step, setStep] = createSignal(0) const form = createForm(() => ({ defaultValues: { step1: { name: '', }, step2: { age: 0, }, }, })) return ( <> <Show when={step() === 0}> <form.FormGroup name="step1" onGroupSubmit={() => { // 校验通过后推进步骤 setStep(step() + 1) }} onGroupSubmitInvalid={() => { // 处理校验未通过的提交,和顶层表单的行为一致 }} onSubmitMeta={{} as SomeType} > {(group) => ( // 使用 `group().handleSubmit()` 提交子表单,但不提交父表单 // ... )} </form.FormGroup> </Show> <Show when={step() === 1}> <form.FormGroup name="step2"> {(group) => ( // 在最后一步,使用 `form.handleSubmit()` 提交整个表单 // ... )} </form.FormGroup> </Show> </> )

这种模式的语义非常清晰:

  • 中间步骤调用group().handleSubmit(),只校验并提交当前分组,通过后由onGroupSubmit推进步骤;
  • 最后一步调用form.handleSubmit(),提交整个父表单(此时父表单会校验所有字段,包括此前步骤中已渲染过的分组数据)。

从源码看,FormGroupApi._handleSubmit的流程是(见 FormGroupApi.ts):先递增submissionAttempts、标记所有相关字段为 touched → 对所有相关字段执行submit校验(validateAllFields('submit'))→ 若字段无效,调用onGroupSubmitInvalid并终止 → 再执行分组自身的submit校验 → 若分组或字段仍无效则调用onGroupSubmitInvalid→ 全部通过后依次触发相关字段的onGroupSubmit监听器、分组的onSubmit监听器,最后执行onGroupSubmit回调并把isSubmitted/isSubmitSuccessful置为true。这就是"子表单提交不影响父表单"的底层保证:_handleSubmit全程只操作this.form中与本分组相关的字段,绝不调用父表单的handleSubmit

真实示例:multi-step-wizard

仓库中的 multi-step-wizard 示例 完整演示了上述模式。它以createFormHook组装出useAppForm/withForm(见 hooks/form.tsx),公共配置通过formOptions声明(见 shared-form.tsx):

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-subform.tsx)把onDynamic: step1Schema挂在分组上,通过onGroupSubmit推进步骤;第二步子表单(step2-subform.tsx)在onGroupSubmit中调用props.form.handleSubmit()完成整表提交。页面层(page.tsx)用createSignal+<Show>step条件渲染两个子表单,父表单的onDynamic只负责整表提交时校验完整 Schema:

const form = useAppForm(() => ({ ...wizardFormOpts, validationLogic: revalidateLogic(), validators: { // onDynamic 仅在 `form.handleSubmit` 被调用时生效; // 调用 FormGroup 的 `handleSubmit` 时只会校验当前步骤的 Schema。 onDynamic: z.object({ step1: step1Schema, step2: step2Schema, }), }, onSubmit: ({ value }) => { alert(`Form submitted: ${JSON.stringify(value)}`) }, }))

Form Group 校验:子表单自己的校验管线

FormGroup 拥有区别于普通字段的独立校验流程,专门为子表单设计,主要体现在三个方面。

1. 分组可以有自己的校验器

和字段一样,FormGroup 支持validators,可直接读取分组级别的错误映射:

<form.FormGroup name="step1" validators={{ onChange: () => 'Error' }}> {(group) => { group().state.meta.errorMap // {onChange: "Error" | undefined} group().state.meta.errors // ("Error")[] }} </form.FormGroup>

FormGroupValidators完整支持以下配置项(见 FormGroupApi.ts):

配置项作用
onMount分组挂载时运行的同步校验
onChange值变化时运行的同步校验
onChangeAsync/onChangeAsyncDebounceMs变化时的异步校验及防抖毫秒数
onBlur/onBlurAsync/onBlurAsyncDebounceMs失焦时的同步 / 异步校验
onSubmit/onSubmitAsync提交时的同步 / 异步校验
onDynamic/onDynamicAsync/onDynamicAsyncDebounceMs动态(Schema 驱动)校验

此外,分组还支持canSubmitWhenInvalid(允许无效状态下提交)、validationLogic(覆盖父表单的校验策略,默认继承父表单或defaultValidationLogic)、listeners(含onChangeonBluronMountonUnmountonSubmitonGroupSubmit)以及onSubmitMetaonGroupSubmitonGroupSubmitInvalid等选项。

2. 可以把错误设置到子字段上

分组校验器可以返回一个{ group, fields }形状的对象,其中fields中的 key 使用相对于该分组的字段名,从而把错误分发(distribute)到对应的子字段:

<form.FormGroup name="step1" validators={{ onChange: ({ value, groupApi }) => ({ group: value.name === 'error' ? 'Group error' : undefined, fields: { // 必须使用相对 FormGroup 的字段名作为错误 key, // 以便与标准 schema 在分组上的工作方式保持一致 name: value.name === 'error' ? 'Field error' : undefined, }, }), }} />

这一分发逻辑在源码中由distributeFieldErrors实现(见 FormGroupApi.ts):它通过buildChildFieldName把相对字段名(支持namenested.value点号形式和[0].name括号形式)转成完全限定名(fully-qualified name),写入子字段的errorMap/errorSourceMap,并记录"上一次分发过的字段名",以便在后续校验运行时清除过期错误,同时不会覆盖父表单校验器设置的错误。

3. 直接接受标准 Schema(如 Zod)

FormGroup 的校验器与标准 Schema(Standard Schema)天然兼容,可以直接传入 Zod 对象:

<form.FormGroup name="step1" validators={{ onChange: z.object({ name: z.string().min(2), }), }} />

从实现上看,runValidator会先通过isStandardSchemaValidator识别标准 Schema,再用standardSchemaValidators执行,并把结果 remap 成{ group, fields }形状以接入分组错误分发管线(见 FormGroupApi.ts 与 remapStandardSchemaResultForGroup)。手动函数校验器则直接返回{ group, fields }

为什么字段错误 key 使用相对路径?

FormGroup 刻意不使用字段的完整路径名,目的是让你能够像搭积木一样组合 Schema:

const step1Schema = z.object({ name: z.string().min(2) }) const schema = z.object({ step1: step1Schema, step2: step2Schema })

然后把step1Schema传给对应的 FormGroup、把schema传给父表单。这样,即使某个分组被绕过(例如用户直接提交整表),部分校验过的数据也仍然会在对应位置报错——两套 Schema 无缝共享同一份校验语义,避免了"分组能过、整表不过"的错位问题。

动态分组校验:把 Schema 挂在 FormGroup 上而非父表单

如果要在 FormGroup 上使用动态校验(onDynamic),请不要依赖createForm上传入的onDynamic校验器:

createForm(() => ({ validationLogic: revalidateLogic(), validators: { // 注意:当子表单被提交时,该校验器不会运行 `onChange`; // 它只会在表单自身被提交时运行 `onChange`。 onDynamic: schema, }, }))

正确做法是把分组对应的子 Schema 传给FormGroup 自身的onDynamic

<form.FormGroup validators={{ onDynamic: step1Schema }} />

此时,group().submissionAttempts会成为切换"提交前 / 提交后"校验策略的依据——这正是revalidateLogic()配合onDynamic判断校验源('field''form')的核心机制:分组第一次提交前按"字段级"逻辑校验,提交失败后按"表单级"逻辑继续校验,确保向导场景下"每步各自校验、最终整表兜底"。

multi-step-wizard 示例正是这种做法的教科书级实现:每个分组分别挂onDynamic: step1Schema/onDynamic: step2Schema,父表单只保留整表提交用的组合 Schema(见 page.tsx 与 step1-subform.tsx)。

Form Group 状态与 meta 聚合标志

除了group().state.meta.errors,你还可以通过group().state.value读取分组的当前值(它等价于父表单状态树中name路径下的子对象)。而group().state.meta中最有价值的是下面几个聚合后的校验标志:

meta 属性含义
group().state.meta.isFieldsValid当所有字段级校验器均无错误时为true
group().state.meta.isGroupValid当分组自身的校验器无错误时为true
group().state.meta.isValid当字段级与分组级校验器均无错误时为true
group().state.meta.isSubmitting当分组正在提交过程中时为true

这些标志都定义在FormGroupMeta接口中(见 FormGroupApi.ts):isFieldsValidatingisFieldsValidisGroupValidisValidcanSubmit,并继承FormGroupState中的isSubmittingisSubmittedisValidatingsubmissionAttemptsisSubmitSuccessfulFormGroupApistore只保存{ value, meta }两份最小状态,聚合推导全部放在父表单的formGroupMetaDerived中完成(见 FormGroupApi.ts),getRelatedFieldMetasDerived会跳过分组自身的条目(避免把组级校验与字段级校验混为一谈),并通过isFieldInGroup收集分组下的所有子字段 meta 进行聚合。

在实际向导中,isSubmitting非常适合用来做"当前步骤提交中"的按钮 loading 状态;isValid/isFieldsValid/isGroupValid则可以驱动"下一步按钮是否可点"或分步指示器的完成态。

小结

TanStack Form 的FormGroup把"多步骤表单"这一高频需求收敛成了声明式的子表单方案:

  • <form.FormGroup name="...">拆分状态树,分组自带表单类 API(handleSubmitdeleteFieldinsertFieldValue等);
  • 中间步骤用group().handleSubmit()局部提交并推进流程,最后一步用form.handleSubmit()整表提交;
  • 分组校验器既可以是函数(返回{ group, fields }分发字段错误),也可以是标准 Schema(Zod 等),字段 key 使用相对分组路径以支持 Schema 组合复用;
  • 动态校验请把子 Schema 挂在分组自身的onDynamic上,配合revalidateLogic()实现"分组内局部校验、整表提交兜底";
  • state.meta提供isFieldsValidisGroupValidisValidisSubmitting等聚合标志,可无缝驱动 UI 状态。

如果想在真实项目中上手,可以直接参考仓库的 multi-step-wizard 示例,对照其shared-form.tsxstep1-subform.tsxstep2-subform.tsxpage.tsx四份文件,即可拼出一个完整、健壮的分步向导表单。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 14:48:36

SCI论文写作操作手册:四段式引言、七步法与投稿自检

简介&#xff1a;这份面向研究生与青年科研人员的宣讲型PPT&#xff0c;聚焦SCI论文写作的方法与心态建设&#xff0c;帮助解决选题构思、结构搭建、数据处理与投稿准备等常见难题。压缩包内含1个ppt文件&#xff0c;约1.03MB&#xff0c;以幻灯片形式系统梳理好论文的六大标准…

作者头像 李华
网站建设 2026/9/17 14:48:23

Open Agents 官方React最佳实践审计:57条规则优化实录

Open Agents 官方React最佳实践审计&#xff1a;57条规则优化实录 【免费下载链接】open-agents An open source template for building cloud agents. 项目地址: https://gitcode.com/GitHub_Trending/op/open-agents Open Agents 是一个在 Vercel 上构建和运行云端编程…

作者头像 李华
网站建设 2026/9/17 14:48:15

Python元组:不可变容器的原理与应用实践

1. 容器与元组基础概念解析在编程领域&#xff0c;容器&#xff08;Container&#xff09;和元组&#xff08;Tuple&#xff09;是两个看似简单却蕴含深意的数据结构。作为Python开发者&#xff0c;我最初接触这两个概念时也曾困惑&#xff1a;为什么有了列表还要元组&#xff…

作者头像 李华
网站建设 2026/9/17 14:44:54

时序逻辑电路与触发器:从双稳态到计数器的记忆原理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 14:41:22

Fluent Bit 内嵌 nghttp2:nghttp2_submit_headers() API 深入解析

Fluent Bit 内嵌 nghttp2&#xff1a;nghttp2_submit_headers() API 深入解析 【免费下载链接】fluent-bit Fast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows 项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit n…

作者头像 李华