diff --git a/.changeset/form-level-validator-resubmit.md b/.changeset/form-level-validator-resubmit.md new file mode 100644 index 000000000..640033d84 --- /dev/null +++ b/.changeset/form-level-validator-resubmit.md @@ -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). diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index d710cd105..ebaaba1f0 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -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() @@ -2488,8 +2498,6 @@ export class FormApi< return } - await this.validate('submit') - // Fields are invalid, do not submit if (!this.state.isValid) { done() diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 024b319f0..c6d163bb3 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -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 = {} + 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