Skip to content

[WIP] Form Groups#2128

Draft
crutchcorn wants to merge 1 commit intomainfrom
form-group
Draft

[WIP] Form Groups#2128
crutchcorn wants to merge 1 commit intomainfrom
form-group

Conversation

@crutchcorn
Copy link
Copy Markdown
Member

@crutchcorn crutchcorn commented Apr 16, 2026

This PR implements #419 as outlined in the issue.

This feature might take us some time to implement due to the sheer amount of edgecases and API surface area we have to cover to mark this idea as stable and ready-for-usage.

We will very likely ask community members to help us test when we get farther along, but will do so in #419.

Thank you for your patience!

Core TODOs

This is what will take up a majority of our time.

  • Core FormGroupAPI class and base usage
  • Fixed types with FormGroupAPI
  • core package integration tests
  • core package type tests

Framework support

This list looks long and intimidating, but once we have a single framework's support checkboxed, it's mostly boilerplate for the rest of the frameworks.

As a result, we will be prioritizing React adapter support as our first target.

  • react adapter base code
  • react adapter integration tests
  • react adapter type tests
  • react adapter examples
  • react adapter docs

  • angular adapter base code
  • angular adapter integration tests
  • angular adapter type tests
  • angular adapter examples
  • angular adapter docs

  • lit adapter base code
  • lit adapter integration tests
  • lit adapter type tests
  • lit adapter examples
  • lit adapter docs

  • solid adapter base code
  • solid adapter integration tests
  • solid adapter type tests
  • solid adapter examples
  • solid adapter docs

  • svelte adapter base code
  • svelte adapter integration tests
  • svelte adapter type tests
  • svelte adapter examples
  • svelte adapter docs

  • vue adapter base code
  • vue adapter integration tests
  • vue adapter type tests
  • vue adapter examples
  • vue adapter docs

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced form group submission handling with group-level orchestration and validation.
    • Added field-level group submit listener support.
    • Enhanced form validation methods with selective field filtering and error map control options.
  • Tests

    • Added test coverage for form group submission workflows.

@crutchcorn crutchcorn marked this pull request as draft April 16, 2026 08:58
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

This pull request introduces form group submission support to the form-core package. A new FormGroupApi class manages group-level form state and submission orchestration. The field listener interface gains an onGroupSubmit hook, and form validation methods are extended to support optional field filtering and conditional error map updates.

Changes

Cohort / File(s) Summary
Field Listener Extensions
packages/form-core/src/FieldApi.ts
Added optional onGroupSubmit listener hook to FieldListeners interface for group-level submission callbacks.
Form Validation API Extensions
packages/form-core/src/FormApi.ts
Added ValidateOpts interface and extended validateSync, validateAsync, and validate methods to accept optional validation options. Enables field filtering via filterFieldNames and conditional form error map updates via dontUpdateFormErrorMap.
Form Group Submission
packages/form-core/src/FormGroupApi.ts
New module introducing FormGroupApi class with group-level state management (isSubmitting, isSubmitted, isValidating, submissionAttempts, isSubmitSuccessful). Core _handleSubmit method orchestrates validation of group-scoped fields, invokes group and field-level submission callbacks, and manages submission state transitions.
Package Exports
packages/form-core/src/index.ts
Added re-export of all public symbols from FormGroupApi module.
Test Coverage
packages/form-core/tests/FormGroupApi.spec.ts
New test suite covering group submission scenarios: valid submissions, form-level validation blocking, and multiple field validation failures. Tests verify correct callback invocation patterns and submission state management.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant GroupApi as FormGroupApi
    participant FieldApis as FieldApi Instances
    participant FormApi
    
    Caller->>GroupApi: handleSubmit()
    
    rect rgba(100, 150, 200, 0.5)
        Note over GroupApi: Initialize submission state
        GroupApi->>GroupApi: Reset flags, increment attempts
        GroupApi->>GroupApi: Mark related fields as touched
        GroupApi->>GroupApi: Set isSubmitting = true
    end
    
    rect rgba(150, 100, 200, 0.5)
        Note over GroupApi,FieldApis: Validate group fields
        GroupApi->>FieldApis: Validate each field for 'submit'
        FieldApis-->>GroupApi: Field validity results
        alt Group fields invalid
            GroupApi->>GroupApi: Set isSubmitting = false
            GroupApi->>GroupApi: Invoke onGroupSubmitInvalid
            GroupApi-->>Caller: Early return
        end
    end
    
    rect rgba(200, 150, 100, 0.5)
        Note over GroupApi,FormApi: Validate form (filtered)
        GroupApi->>FormApi: validate('submit', {dontUpdateFormErrorMap, filterFieldNames})
        FormApi-->>GroupApi: Form validation result
        alt Form invalid for group fields
            GroupApi->>GroupApi: Set isSubmitting = false
            GroupApi->>GroupApi: Invoke onGroupSubmitInvalid
            GroupApi-->>Caller: Early return
        end
    end
    
    rect rgba(100, 200, 150, 0.5)
        Note over GroupApi,FieldApis: Execute group submission
        GroupApi->>FieldApis: Trigger onGroupSubmit listeners
        GroupApi->>GroupApi: Invoke options.listeners.onSubmit
        GroupApi->>GroupApi: Execute options.onGroupSubmit (try/catch)
        alt Submission succeeds
            GroupApi->>GroupApi: Set isSubmitted = true, isSubmitSuccessful = true
        else Submission fails
            GroupApi->>GroupApi: Set isSubmitSuccessful = false
            GroupApi->>GroupApi: Set isSubmitting = false
            GroupApi-->>Caller: Rethrow error
        end
        GroupApi->>GroupApi: Set isSubmitting = false
    end
    
    GroupApi-->>Caller: Promise resolved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops of joy for groups that form,
Fields aligned in submission norm,
State machines that dance and sway,
Group submissions find their way! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title '[WIP] Form Groups' is vague and doesn't clearly convey the specific nature of the changes made in this PR. Replace '[WIP] Form Groups' with a more descriptive title that captures the main implementation, such as 'feat: add FormGroupApi class for form group submission handling' or 'feat: implement core FormGroupApi for field grouping and submission'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the implementation work completed, outstanding TODOs, framework support roadmap, and appropriate context linking to issue #419.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch form-group

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Apr 16, 2026

View your CI Pipeline Execution ↗ for commit 9edad01

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 21s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 33s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-16 09:00:20 UTC

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 16, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2128

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2128

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2128

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2128

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2128

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2128

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2128

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2128

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2128

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2128

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2128

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2128

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2128

commit: 9edad01

@sentry
Copy link
Copy Markdown

sentry bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 91.30435% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.27%. Comparing base (6892ed0) to head (9edad01).
⚠️ Report is 167 commits behind head on main.

Files with missing lines Patch % Lines
packages/form-core/src/FormGroupApi.ts 90.78% 7 Missing ⚠️
packages/form-core/src/FormApi.ts 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2128      +/-   ##
==========================================
- Coverage   90.35%   90.27%   -0.09%     
==========================================
  Files          38       50      +12     
  Lines        1752     2128     +376     
  Branches      444      552     +108     
==========================================
+ Hits         1583     1921     +338     
- Misses        149      187      +38     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/form-core/tests/FormGroupApi.spec.ts (1)

73-76: Consider adding assertion for field error state.

The test verifies callback invocations but doesn't assert that the field actually has errors after validation. Adding such an assertion would strengthen the test:

expect(step1NameField.state.meta.errors.length).toBeGreaterThan(0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/tests/FormGroupApi.spec.ts` around lines 73 - 76, Add an
assertion to verify the field's error state after validation: after invoking
validation callbacks in the test, assert that step1NameField.state.meta.errors
contains at least one error (e.g., use an assertion like checking length > 0) so
the test not only checks callback invocation but also that FieldApi
(step1NameField) actually recorded validation errors.
packages/form-core/src/FormGroupApi.ts (2)

124-126: Empty mount() method may need cleanup logic later.

The mount method currently does nothing and returns an empty cleanup function. As the implementation matures, consider whether mount should:

  • Subscribe to form state changes
  • Initialize group-specific state
  • Register the group with the form for coordinated lifecycle management
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/src/FormGroupApi.ts` around lines 124 - 126, The mount()
method on FormGroupApi is currently a no-op; replace it with initialization and
teardown logic: when mounting the group, perform any group-specific state
initialization, subscribe to the parent form's change events (or call the form's
registration API such as registerGroup or similar), and return a cleanup
function that unsubscribes and deregisters the group (e.g., call
unregisterGroup/removeGroup) to avoid leaks; ensure you reference the
FormGroupApi instance state and the form-level APIs used to subscribe/register
so the returned function reverses those actions.

216-220: Placeholder values (value: 0) need to be replaced with actual group values.

Multiple callback invocations use value: 0 as a placeholder. This is noted in comments but tracking for completeness. Consider using a more explicit placeholder like undefined or extracting the actual group value subset.

Also applies to: 234-238, 275-278, 281-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/src/FormGroupApi.ts` around lines 216 - 220, The callback
invocations currently pass a placeholder value (value: 0) instead of the actual
group values; replace those placeholders in the onGroup* calls (e.g.,
this.options.onGroupSubmitInvalid, the similar calls at the ranges 234-238,
275-278, 281-285) with the real subset of form state for this group by
extracting the group's values from this.state.values (for example build a
groupValues object by selecting the group's field keys from this.state.values)
and pass that object as value, or if no values exist pass undefined explicitly;
ensure you reference the group field list used by this class when building the
subset so the callbacks receive accurate group data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/form-core/src/FormGroupApi.ts`:
- Around line 274-285: Remove the duplicate invocation of
this.options.onGroupSubmit in FormGroupApi: delete the redundant first call and
keep a single call that passes the real submit payload (use this.state.values
instead of 0 and submitMetaArg instead of {} if those are the intended
variables). Ensure only one this.options.onGroupSubmit is called and it uses the
correct arguments (this.state.values, this.options.form, submitMetaArg).
- Around line 128-131: The startsWith check in _isFieldNamePartOfGroup
incorrectly matches names like "step10" for group "step1"; update
_isFieldNamePartOfGroup (and use this.options.name) to verify a proper boundary
after the prefix: return true only if fieldName === prefix or fieldName
startsWith(prefix + '.') or fieldName startsWith(prefix + '[') (i.e., check that
the character immediately after the prefix is absent or is a separator like '.'
or '[') so unrelated names aren't included.
- Around line 225-241: The code validates group-scoped fields via
this.options.form.validate(..., filterFieldNames: this._isFieldNamePartOfGroup)
but then checks the entire form state with this.options.form.state.isValid;
replace that check with a call to this._isFieldsValid() so the submission
decision uses the same group-scoped validation logic (keeping the done() call
and the this.options.onGroupSubmitInvalid invocation intact), i.e., use
_isFieldsValid() instead of reading form.state.isValid after the filtered
validate call.

In `@packages/form-core/tests/FormGroupApi.spec.ts`:
- Around line 49-60: The validator in the test's onSubmit returns a nested
fields object (step1: { name: { required: true } }) but
GlobalFormValidationError expects flat field paths mapping to ValidationError;
update the onSubmit return to use flat keys like 'step1.name': { required: true
} (i.e., fields: { 'step1.name': { required: true } }) so
Object.keys(fieldErrors) yields the full field path and each value is a proper
ValidationError per the GlobalFormValidationError /
Partial<Record<DeepKeys<TFormData>, ValidationError>> shape.

---

Nitpick comments:
In `@packages/form-core/src/FormGroupApi.ts`:
- Around line 124-126: The mount() method on FormGroupApi is currently a no-op;
replace it with initialization and teardown logic: when mounting the group,
perform any group-specific state initialization, subscribe to the parent form's
change events (or call the form's registration API such as registerGroup or
similar), and return a cleanup function that unsubscribes and deregisters the
group (e.g., call unregisterGroup/removeGroup) to avoid leaks; ensure you
reference the FormGroupApi instance state and the form-level APIs used to
subscribe/register so the returned function reverses those actions.
- Around line 216-220: The callback invocations currently pass a placeholder
value (value: 0) instead of the actual group values; replace those placeholders
in the onGroup* calls (e.g., this.options.onGroupSubmitInvalid, the similar
calls at the ranges 234-238, 275-278, 281-285) with the real subset of form
state for this group by extracting the group's values from this.state.values
(for example build a groupValues object by selecting the group's field keys from
this.state.values) and pass that object as value, or if no values exist pass
undefined explicitly; ensure you reference the group field list used by this
class when building the subset so the callbacks receive accurate group data.

In `@packages/form-core/tests/FormGroupApi.spec.ts`:
- Around line 73-76: Add an assertion to verify the field's error state after
validation: after invoking validation callbacks in the test, assert that
step1NameField.state.meta.errors contains at least one error (e.g., use an
assertion like checking length > 0) so the test not only checks callback
invocation but also that FieldApi (step1NameField) actually recorded validation
errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac7e0310-fbaa-4485-b402-aece0b9fb522

📥 Commits

Reviewing files that changed from the base of the PR and between 254f157 and 9edad01.

📒 Files selected for processing (5)
  • packages/form-core/src/FieldApi.ts
  • packages/form-core/src/FormApi.ts
  • packages/form-core/src/FormGroupApi.ts
  • packages/form-core/src/index.ts
  • packages/form-core/tests/FormGroupApi.spec.ts

Comment on lines +128 to +131
_isFieldNamePartOfGroup = (fieldName: string) => {
// TODO: Does this `startWith` capture sub-field names properly? Probably not. :(
return fieldName.startsWith(this.options.name)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

startsWith check can incorrectly match unrelated fields.

If the group name is "step1", then "step10.name".startsWith("step1") returns true, incorrectly including step10 fields in the group. Add a boundary check:

 _isFieldNamePartOfGroup = (fieldName: string) => {
-  // TODO: Does this `startWith` capture sub-field names properly? Probably not. :(
-  return fieldName.startsWith(this.options.name)
+  const name = this.options.name
+  return (
+    fieldName === name ||
+    fieldName.startsWith(`${name}.`) ||
+    fieldName.startsWith(`${name}[`)
+  )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_isFieldNamePartOfGroup = (fieldName: string) => {
// TODO: Does this `startWith` capture sub-field names properly? Probably not. :(
return fieldName.startsWith(this.options.name)
}
_isFieldNamePartOfGroup = (fieldName: string) => {
const name = this.options.name
return (
fieldName === name ||
fieldName.startsWith(`${name}.`) ||
fieldName.startsWith(`${name}[`)
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/src/FormGroupApi.ts` around lines 128 - 131, The
startsWith check in _isFieldNamePartOfGroup incorrectly matches names like
"step10" for group "step1"; update _isFieldNamePartOfGroup (and use
this.options.name) to verify a proper boundary after the prefix: return true
only if fieldName === prefix or fieldName startsWith(prefix + '.') or fieldName
startsWith(prefix + '[') (i.e., check that the character immediately after the
prefix is absent or is a separator like '.' or '[') so unrelated names aren't
included.

Comment on lines +225 to +241
await this.options.form.validate('submit', {
dontUpdateFormErrorMap: true,
filterFieldNames: this._isFieldNamePartOfGroup as never,
})

// Form is invalid, do not submit
if (!this.options.form.state.isValid) {
done()

this.options.onGroupSubmitInvalid?.({
value: 0 /* this.state.values */,
formApi: this.options.form,
meta: {} as never /* submitMetaArg */,
})

return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how isValid is computed in FormApi to understand dependencies
ast-grep --pattern 'isValid = $_'

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# Find FormApi file
fd -t f "FormApi" --type f

Repository: TanStack/form

Length of output: 468


🏁 Script executed:

# Search for how isValid is defined/computed in the codebase
rg "isValid" packages/form-core/src/ -B 2 -A 2 | head -100

Repository: TanStack/form

Length of output: 6007


🏁 Script executed:

# Look at the validate method to understand dontUpdateFormErrorMap parameter
rg "dontUpdateFormErrorMap" packages/form-core/src/ -B 5 -A 5

Repository: TanStack/form

Length of output: 3499


🏁 Script executed:

# Look at FormApi implementation to see how isValid is determined
cat packages/form-core/src/FormApi.ts | head -300

Repository: TanStack/form

Length of output: 8428


🏁 Script executed:

# Search for where isValid state is set/computed in FormApi
rg "state.isValid|isValid:" packages/form-core/src/FormApi.ts -B 5 -A 5

Repository: TanStack/form

Length of output: 945


🏁 Script executed:

# Search for the actual computation of form's isValid state
rg "isValid\s*:" packages/form-core/src/FormApi.ts -B 10 -A 2 | head -80

Repository: TanStack/form

Length of output: 1027


🏁 Script executed:

# Look for getter or computed property definitions
rg "get isValid|isValid\(\)" packages/form-core/src/FormApi.ts -B 3 -A 5

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# Check how field errors affect form-level isValid
rg "fieldErrors|fieldMetaBase" packages/form-core/src/FormApi.ts -B 2 -A 2 | head -100

Repository: TanStack/form

Length of output: 3473


🏁 Script executed:

# Search for the FormState type definition and isValid computation
rg "interface FormState|type FormState" packages/form-core/src/FormApi.ts -A 30

Repository: TanStack/form

Length of output: 1139


🏁 Script executed:

# Search for the state getter or the store structure
rg "get state|this.state|this.baseStore.state" packages/form-core/src/FormApi.ts | head -40

Repository: TanStack/form

Length of output: 1867


🏁 Script executed:

# Look for where isValid is calculated from field metadata
rg "fieldMeta.*every|\.every.*isValid" packages/form-core/src/ -B 3 -A 3

Repository: TanStack/form

Length of output: 803


🏁 Script executed:

# Search for how filterFieldNames is used in validate method
rg "filterFieldNames" packages/form-core/src/FormApi.ts -B 5 -A 10

Repository: TanStack/form

Length of output: 1773


🏁 Script executed:

# Look at the complete validate method to see the full context
sed -n '800,1000p' packages/form-core/src/FormApi.ts

Repository: TanStack/form

Length of output: 5168


🏁 Script executed:

# Look at the FormGroupApi submit method more completely
sed -n '200,300p' packages/form-core/src/FormGroupApi.ts

Repository: TanStack/form

Length of output: 2723


🏁 Script executed:

# Check if there are any tests that show the expected behavior
rg "dontUpdateFormErrorMap|FilterFieldNames" packages/form-core/tests/ -B 3 -A 5

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# Check the complete validation flow and understand the two checks
sed -n '180,290p' packages/form-core/src/FormGroupApi.ts

Repository: TanStack/form

Length of output: 3104


🏁 Script executed:

# Look at DerivedFormState to see exactly how isValid is computed
rg "interface DerivedFormState|type DerivedFormState" packages/form-core/src/ -A 50 | head -100

Repository: TanStack/form

Length of output: 3722


🏁 Script executed:

# Check if there are any other fields added to the form alongside the FormGroup
rg "FormGroup|FieldGroup" packages/form-core/tests/ -B 3 -A 10 | head -100

Repository: TanStack/form

Length of output: 6601


🏁 Script executed:

# Look at the exact implementation of isValid computation
rg "isFieldsValid = fieldMetaValues.every" packages/form-core/src/FormApi.ts -B 5 -A 15

Repository: TanStack/form

Length of output: 800


🏁 Script executed:

# Check how validate updates field errors and if it respects filterFieldNames
rg "allFieldsToProcess|fieldErrors\[field\]" packages/form-core/src/FormApi.ts -B 2 -A 8 | head -80

Repository: TanStack/form

Length of output: 767


Use _isFieldsValid() to maintain group-scoped validation consistency.

The second validation check should use _isFieldsValid() instead of form.state.isValid. After calling form.validate('submit', { dontUpdateFormErrorMap: true, filterFieldNames: ... }) to validate only group-filtered fields, checking form.state.isValid will fail if unrelated form fields have errors. Since the first check already validates group fields with _isFieldsValid(), the second check should do the same for consistency:

Suggested change
-if (!this.options.form.state.isValid) {
+if (!this._isFieldsValid()) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.options.form.validate('submit', {
dontUpdateFormErrorMap: true,
filterFieldNames: this._isFieldNamePartOfGroup as never,
})
// Form is invalid, do not submit
if (!this.options.form.state.isValid) {
done()
this.options.onGroupSubmitInvalid?.({
value: 0 /* this.state.values */,
formApi: this.options.form,
meta: {} as never /* submitMetaArg */,
})
return
}
await this.options.form.validate('submit', {
dontUpdateFormErrorMap: true,
filterFieldNames: this._isFieldNamePartOfGroup as never,
})
// Form is invalid, do not submit
if (!this._isFieldsValid()) {
done()
this.options.onGroupSubmitInvalid?.({
value: 0 /* this.state.values */,
formApi: this.options.form,
meta: {} as never /* submitMetaArg */,
})
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/src/FormGroupApi.ts` around lines 225 - 241, The code
validates group-scoped fields via this.options.form.validate(...,
filterFieldNames: this._isFieldNamePartOfGroup) but then checks the entire form
state with this.options.form.state.isValid; replace that check with a call to
this._isFieldsValid() so the submission decision uses the same group-scoped
validation logic (keeping the done() call and the
this.options.onGroupSubmitInvalid invocation intact), i.e., use _isFieldsValid()
instead of reading form.state.isValid after the filtered validate call.

Comment on lines +274 to +285
await this.options.onGroupSubmit?.({
value: 0,
formApi: this.options.form,
meta: {},
})

// Run the submit code
await this.options.onGroupSubmit?.({
value: 0, // this.state.values,
formApi: this.options.form,
meta: {}, // submitMetaArg,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Duplicate onGroupSubmit invocation.

onGroupSubmit is called twice in succession (lines 274-278 and 281-285). This appears to be a copy-paste error. Remove one of the calls:

 try {
-  await this.options.onGroupSubmit?.({
-    value: 0,
-    formApi: this.options.form,
-    meta: {},
-  })
-
   // Run the submit code
   await this.options.onGroupSubmit?.({
     value: 0, // this.state.values,
     formApi: this.options.form,
     meta: {}, // submitMetaArg,
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.options.onGroupSubmit?.({
value: 0,
formApi: this.options.form,
meta: {},
})
// Run the submit code
await this.options.onGroupSubmit?.({
value: 0, // this.state.values,
formApi: this.options.form,
meta: {}, // submitMetaArg,
})
// Run the submit code
await this.options.onGroupSubmit?.({
value: 0, // this.state.values,
formApi: this.options.form,
meta: {}, // submitMetaArg,
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/src/FormGroupApi.ts` around lines 274 - 285, Remove the
duplicate invocation of this.options.onGroupSubmit in FormGroupApi: delete the
redundant first call and keep a single call that passes the real submit payload
(use this.state.values instead of 0 and submitMetaArg instead of {} if those are
the intended variables). Ensure only one this.options.onGroupSubmit is called
and it uses the correct arguments (this.state.values, this.options.form,
submitMetaArg).

Comment on lines +49 to +60
onSubmit: () => {
return {
fields: {
step1: {
name: {
required: true,
},
},
},
}
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how field errors are structured in existing tests
rg -n "fields:" packages/form-core/tests/ -A 5 --type=ts | head -60

Repository: TanStack/form

Length of output: 4099


🏁 Script executed:

# Find and examine the normalizeError function in FormApi.ts
rg -n "normalizeError" packages/form-core/src/ -A 10 --type=ts

Repository: TanStack/form

Length of output: 6024


🏁 Script executed:

# Check the full test context to understand what assertions are made
sed -n '39,87p' packages/form-core/tests/FormGroupApi.spec.ts

Repository: TanStack/form

Length of output: 1118


🏁 Script executed:

# Check if nested object error format is used elsewhere in tests
rg -n "required: true" packages/form-core/tests/ --type=ts

Repository: TanStack/form

Length of output: 307


🏁 Script executed:

# Check how fieldErrors are processed and matched to fields
rg -n "fieldErrors" packages/form-core/src/FormApi.ts -B 2 -A 3 | head -80

Repository: TanStack/form

Length of output: 2834


🏁 Script executed:

# Look at FormGroupApi to see how it handles field errors
cat packages/form-core/src/FormGroupApi.ts | head -100

Repository: TanStack/form

Length of output: 2550


🏁 Script executed:

# Check field error assignment logic
rg -n "setFieldMeta\|errorMap" packages/form-core/src/FormApi.ts -B 1 -A 2 | head -50

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# Get the full test and see what field is being checked
sed -n '39,90p' packages/form-core/tests/FormGroupApi.spec.ts

Repository: TanStack/form

Length of output: 1269


🏁 Script executed:

# Check if step1NameField.state.errors is asserted
rg -n "step1NameField" packages/form-core/tests/FormGroupApi.spec.ts -A 2 -B 1 | head -30

Repository: TanStack/form

Length of output: 655


🏁 Script executed:

# Check if there are any assertions on step1NameField.state.meta or errors
rg -n "step1NameField\." packages/form-core/tests/FormGroupApi.spec.ts | head -20

Repository: TanStack/form

Length of output: 147


🏁 Script executed:

# Check the type definition of ValidationError to understand what structure is expected
rg -n "type ValidationError" packages/form-core/src/ -A 3

Repository: TanStack/form

Length of output: 1049


🏁 Script executed:

# Check how the error is actually processed - look for any recursive processing of nested errors
rg -n "determineFormLevelErrorSourceAndValue" packages/form-core/src/FormApi.ts -A 20 | head -60

Repository: TanStack/form

Length of output: 2709


🏁 Script executed:

# Also check if there's any flattening of nested error structures
rg -n "flatten\|flat\|nested" packages/form-core/src/FormApi.ts | head -20

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# Check the GlobalFormValidationError type definition
rg -n "GlobalFormValidationError\|isGlobalFormValidationError" packages/form-core/src/ -B 2 -A 5 | head -80

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# See real usage examples in other validators
rg -n "return {" packages/form-core/tests/ -A 8 --type=ts | grep -A 8 "fields:" | head -50

Repository: TanStack/form

Length of output: 136


🏁 Script executed:

# Check the GlobalFormValidationError type definition
rg -n "GlobalFormValidationError" packages/form-core/src/utils.ts -B 2 -A 8

Repository: TanStack/form

Length of output: 732


🏁 Script executed:

# Search for validator return examples more carefully
rg -n "fields:" packages/form-core/tests/FormGroupApi.spec.ts -B 2 -A 4

Repository: TanStack/form

Length of output: 441


🏁 Script executed:

# Check the GlobalFormValidationError type definition in types.ts
rg -n "GlobalFormValidationError" packages/form-core/src/types.ts -B 2 -A 8

Repository: TanStack/form

Length of output: 1352


🏁 Script executed:

# Check what the determineFormLevelErrorSourceAndValue function does
rg -n "export.*determineFormLevelErrorSourceAndValue" packages/form-core/src/utils.ts -A 30

Repository: TanStack/form

Length of output: 1166


🏁 Script executed:

# Let me verify one more thing - check if field name iteration includes both nested and flat keys
sed -n '1720,1750p' packages/form-core/src/FormApi.ts

Repository: TanStack/form

Length of output: 1083


🏁 Script executed:

# Also check test file to see if there are assertions on field.state properties anywhere
rg -n "expect.*step1NameField" packages/form-core/tests/FormGroupApi.spec.ts

Repository: TanStack/form

Length of output: 39


Fix the validator error format to use flat field paths with proper ValidationError values.

The validator returns { fields: { step1: { name: { required: true } } } }, but GlobalFormValidationError expects field keys to be flat paths matching field names:

fields: Partial<Record<DeepKeys<TFormData>, ValidationError>>

With the current nested structure, the error for step1.name is not properly mapped because:

  • Object.keys(fieldErrors) yields ['step1'] instead of ['step1.name']
  • When checking fieldErrors['step1.name'], it returns undefined (error is lost)
  • The field receives the nested object { name: { required: true } } instead of a proper ValidationError

Correct the validator to return:

validators: {
  onSubmit: () => {
    return {
      fields: {
-       step1: {
-         name: {
-           required: true,
-         },
-       },
+       'step1.name': 'Name is required',
      },
    }
  },
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/tests/FormGroupApi.spec.ts` around lines 49 - 60, The
validator in the test's onSubmit returns a nested fields object (step1: { name:
{ required: true } }) but GlobalFormValidationError expects flat field paths
mapping to ValidationError; update the onSubmit return to use flat keys like
'step1.name': { required: true } (i.e., fields: { 'step1.name': { required: true
} }) so Object.keys(fieldErrors) yields the full field path and each value is a
proper ValidationError per the GlobalFormValidationError /
Partial<Record<DeepKeys<TFormData>, ValidationError>> shape.

@crutchcorn
Copy link
Copy Markdown
Member Author

crutchcorn commented Apr 16, 2026

Note to self: We need to extend FormGroupApi to add implements FieldManipulator as part of our Form-like contract.

@crutchcorn
Copy link
Copy Markdown
Member Author

Another note to self: Work on the validators for FormGroupApi live in this branch: form-group-validation

To ensure that this PR is clean enough to review by other maintainers. Will merge these two branches when I'm more confident about that code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant