ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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 FormSolid 适配器的 Form Group表单分组为核心讲解如何用form.FormGroup把大型多步骤表单拆分为独立的子表单并让每个子表单拥有独立的校验、错误分发与提交语义。读完本文你将掌握 FormGroup 的声明式用法、组级校验与标准 Schema 组合策略、onDynamic动态校验的推荐姿势以及group().state.meta聚合状态的使用方法可直接套用到多步骤向导Multi-step Wizard等真实场景。为什么需要 FormGroup多步骤表单的拆分痛点当构建一个包含多个步骤的表单如分步向导、流程式填写页面时每个步骤如果各自维护一个独立的顶层表单往往会把表单提交与校验流程复杂化——你需要手工拼接各步骤的数据、分别触发校验、再协调最终提交逻辑冗长且容易出错。TanStack Form 为此提供了内建的子表单能力form.FormGroup。它允许你在一个统一的form实例内部声明多个子表单sub-form每个子表单拥有近似于表单的 API如deleteField、insertFieldValue、handleSubmit同时又共享父表单的单一状态树让这类开发变得非常简单。如下是一个典型的多步骤向导界面这正是 FormGroup 最典型的应用场景基本用法声明一个 FormGroup使用 FormGroup 的方式和Field几乎一致通过createForm或useAppForm即createFormHook组合出来的表单创建form变量然后引用其FormGroup组件const form createForm(() ({ defaultValues: { step1: { name: , }, step2: { age: 0, }, }, })) return ( form.FormGroup namestep1 {(group) ( // group() 拥有完整的表单类方法 // 例如 deleteField、insertFieldValue 等 // ... )} /form.FormGroup )这里有几个关键点name必须指向defaultValues中的一个深层路径DeepKeysTParentData它确定了该分组在父表单状态树中的挂载位置渲染函数render prop接收一个访问器accessorgroup调用group()会得到一个FormGroupApi实例FormGroupApi同时实现了FormLikeAPI与FieldLikeAPI见 FormGroupApi.ts所以它既具备表单的方法如handleSubmit也具备字段的方法如setValue、validate并且所有操作最终都委托给父表单的FormApi如deleteField内部调用this.form.deleteField。在 Solid 适配器中form.FormGroup是createForm返回的扩展 API 的一部分实现位于 createForm.tsxextendedApi.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 namestep1 onGroupSubmit{() { // 校验通过后推进步骤 setStep(step() 1) }} onGroupSubmitInvalid{() { // 处理校验未通过的提交和顶层表单的行为一致 }} onSubmitMeta{{} as SomeType} {(group) ( // 使用 group().handleSubmit() 提交子表单但不提交父表单 // ... )} /form.FormGroup /Show Show when{step() 1} form.FormGroup namestep2 {(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.tsxexport 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用createSignalShow按step条件渲染两个子表单父表单的onDynamic只负责整表提交时校验完整 Schemaconst 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 namestep1 validators{{ onChange: () Error }} {(group) { group().state.meta.errorMap // {onChange: Error | undefined} group().state.meta.errors // (Error)[] }} /form.FormGroupFormGroupValidators完整支持以下配置项见 FormGroupApi.ts配置项作用onMount分组挂载时运行的同步校验onChange值变化时运行的同步校验onChangeAsync/onChangeAsyncDebounceMs变化时的异步校验及防抖毫秒数onBlur/onBlurAsync/onBlurAsyncDebounceMs失焦时的同步 / 异步校验onSubmit/onSubmitAsync提交时的同步 / 异步校验onDynamic/onDynamicAsync/onDynamicAsyncDebounceMs动态Schema 驱动校验此外分组还支持canSubmitWhenInvalid允许无效状态下提交、validationLogic覆盖父表单的校验策略默认继承父表单或defaultValidationLogic、listeners含onChange、onBlur、onMount、onUnmount、onSubmit、onGroupSubmit以及onSubmitMeta、onGroupSubmit、onGroupSubmitInvalid等选项。2. 可以把错误设置到子字段上分组校验器可以返回一个{ group, fields }形状的对象其中fields中的 key 使用相对于该分组的字段名从而把错误分发distribute到对应的子字段form.FormGroup namestep1 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把相对字段名支持name、nested.value点号形式和[0].name括号形式转成完全限定名fully-qualified name写入子字段的errorMap/errorSourceMap并记录上一次分发过的字段名以便在后续校验运行时清除过期错误同时不会覆盖父表单校验器设置的错误。3. 直接接受标准 Schema如 ZodFormGroup 的校验器与标准 SchemaStandard Schema天然兼容可以直接传入 Zod 对象form.FormGroup namestep1 validators{{ onChange: z.object({ name: z.string().min(2), }), }} /从实现上看runValidator会先通过isStandardSchemaValidator识别标准 Schema再用standardSchemaValidators执行并把结果 remap 成{ group, fields }形状以接入分组错误分发管线见 FormGroupApi.ts 与 remapStandardSchemaResultForGroup。手动函数校验器则直接返回{ group, fields }。为什么字段错误 key 使用相对路径FormGroup 刻意不使用字段的完整路径名目的是让你能够像搭积木一样组合 Schemaconst 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 自身的onDynamicform.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当所有字段级校验器均无错误时为truegroup().state.meta.isGroupValid当分组自身的校验器无错误时为truegroup().state.meta.isValid当字段级与分组级校验器均无错误时为truegroup().state.meta.isSubmitting当分组正在提交过程中时为true这些标志都定义在FormGroupMeta接口中见 FormGroupApi.tsisFieldsValidating、isFieldsValid、isGroupValid、isValid、canSubmit并继承FormGroupState中的isSubmitting、isSubmitted、isValidating、submissionAttempts、isSubmitSuccessful。FormGroupApi的store只保存{ value, meta }两份最小状态聚合推导全部放在父表单的formGroupMetaDerived中完成见 FormGroupApi.tsgetRelatedFieldMetasDerived会跳过分组自身的条目避免把组级校验与字段级校验混为一谈并通过isFieldInGroup收集分组下的所有子字段 meta 进行聚合。在实际向导中isSubmitting非常适合用来做当前步骤提交中的按钮 loading 状态isValid/isFieldsValid/isGroupValid则可以驱动下一步按钮是否可点或分步指示器的完成态。小结TanStack Form 的FormGroup把多步骤表单这一高频需求收敛成了声明式的子表单方案用form.FormGroup name...拆分状态树分组自带表单类 APIhandleSubmit、deleteField、insertFieldValue等中间步骤用group().handleSubmit()局部提交并推进流程最后一步用form.handleSubmit()整表提交分组校验器既可以是函数返回{ group, fields }分发字段错误也可以是标准 SchemaZod 等字段 key 使用相对分组路径以支持 Schema 组合复用动态校验请把子 Schema 挂在分组自身的onDynamic上配合revalidateLogic()实现分组内局部校验、整表提交兜底state.meta提供isFieldsValid、isGroupValid、isValid、isSubmitting等聚合标志可无缝驱动 UI 状态。如果想在真实项目中上手可以直接参考仓库的 multi-step-wizard 示例对照其shared-form.tsx、step1-subform.tsx、step2-subform.tsx与page.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),仅供参考
返回列表