Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/form-level-validator-resubmit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/form-core": patch
---

Re-run a form-level `onSubmit` validator on every submit (when no field-level validator errored) so it can recompute and clear its own field errors. Previously a form configured with only a form-level validator could be permanently gated by stale errors, because `_handleSubmit` bailed on `isFieldsValid` before the form-level validator re-ran (#2248).
14 changes: 11 additions & 3 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,17 @@ export class FormApi<
this.baseStore.setState((prev) => ({ ...prev, isSubmitting: false }))
}

await this.validateAllFields('submit')
const fieldErrors = await this.validateAllFields('submit')

// A form-level validator owns its field errors (via `{ fields }`), so it must
// re-run on every submit to recompute/clear them; otherwise stale errors from
// a prior submit keep `isFieldsValid` false and gate the form off permanently
// when there are no field-level validators to clear them (#2248). Only run it
// here when no field-level validator errored this submit — if the fields are
// already invalid on their own, the form validator is skipped as before.
if (fieldErrors.length === 0) {
await this.validate('submit')
}

if (!this.state.isFieldsValid) {
done()
Expand All @@ -2488,8 +2498,6 @@ export class FormApi<
return
}

await this.validate('submit')

// Fields are invalid, do not submit
if (!this.state.isValid) {
done()
Expand Down
37 changes: 37 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1944,6 +1944,43 @@ describe('form api', () => {
)
})

it('re-runs a form-level onSubmit validator on re-submission when only a form-level validator exists (#2248)', async () => {
const onSubmit = vi.fn()
const form = new FormApi({
defaultValues: { name: '', other: '' },
validators: {
onSubmit: ({ value }) => {
const fields: Record<string, string> = {}
if (!value.name) fields.name = 'Required'
if (!value.other) fields.other = 'Required'
return Object.keys(fields).length ? { fields } : undefined
},
},
canSubmitWhenInvalid: true,
onSubmit,
})
form.mount()

// 1) Submit empty -> the form-level validator flags both fields.
await form.handleSubmit()
expect(form.state.fieldMeta.name?.errorMap.onSubmit).toBe('Required')
expect(form.state.fieldMeta.other?.errorMap.onSubmit).toBe('Required')
expect(onSubmit).not.toHaveBeenCalled()

// 2) Fill both fields so the form-level validator would now pass.
form.setFieldValue('name', 'John')
form.setFieldValue('other', 'thing')

// 3) Submit again -> the form-level validator must re-run, clear the stale
// errors, and the form should submit. Currently `_handleSubmit` bails on
// the stale `isFieldsValid === false` before `validate('submit')` re-runs,
// so the errors never clear and the form can never be submitted.
await form.handleSubmit()
expect(form.state.fieldMeta.name?.errorMap.onSubmit).toBeUndefined()
expect(form.state.fieldMeta.other?.errorMap.onSubmit).toBeUndefined()
expect(onSubmit).toHaveBeenCalledTimes(1)
})

it('should run field-level onBlur validators on re-submission to clear stale errors', async () => {
// Regression: stale onBlur errors could prevent re-submission because
// canSubmit became false and _handleSubmit returned early before running
Expand Down