diff --git a/.changeset/fix-stale-onblur-resubmit.md b/.changeset/fix-stale-onblur-resubmit.md new file mode 100644 index 000000000..134c195b6 --- /dev/null +++ b/.changeset/fix-stale-onblur-resubmit.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +fix(form-core): clear stale onBlur errors on re-submission diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 2317fb2b9..a7e4fdaae 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2105,12 +2105,19 @@ export class FormApi< submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta) if (!this.state.canSubmit && !this._devtoolsSubmissionOverride) { - this.options.onSubmitInvalid?.({ - value: this.state.values, - formApi: this, - meta: submitMetaArg, - }) - return + // On re-submission (submissionAttempts > 1), skip the early return so + // validateAllFields can re-run and clear stale field errors (e.g. from a + // previous onBlur validation that is no longer relevant). The + // isFieldsValid check below will call onSubmitInvalid if the form is + // still invalid after re-validation. + if (this.baseStore.state.submissionAttempts <= 1) { + this.options.onSubmitInvalid?.({ + value: this.state.values, + formApi: this, + meta: submitMetaArg, + }) + return + } } this.baseStore.setState((d) => ({ ...d, isSubmitting: true })) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index eccd2e544..5927aea19 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -1873,6 +1873,53 @@ describe('form api', () => { expect(formSubmit).toHaveBeenCalledOnce() }) + 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 + // validateAllFields, which would have re-evaluated and cleared the error. + // See: https://github.com/TanStack/form/issues/2034 + const onSubmit = vi.fn() + const onSubmitInvalid = vi.fn() + + const form = new FormApi({ + defaultValues: { type: 'PIN', pin: '' }, + onSubmit, + onSubmitInvalid, + }) + form.mount() + + // PIN field with an onBlur validator that is only required when type === 'PIN' + const pinField = new FieldApi({ + form, + name: 'pin', + validators: { + onBlur: ({ value }) => + form.getFieldValue('type') === 'PIN' && !value + ? 'PIN is required' + : undefined, + }, + }) + pinField.mount() + + // Simulate user touching and blurring the PIN field while type is 'PIN' + pinField.handleBlur() + expect(pinField.state.meta.errorMap.onBlur).toBe('PIN is required') + + // First submit: form is invalid, onSubmitInvalid is called + await form.handleSubmit() + expect(onSubmitInvalid).toHaveBeenCalledTimes(1) + expect(onSubmit).not.toHaveBeenCalled() + + // User switches type to 'Card' — PIN is no longer required + form.setFieldValue('type', 'Card') + + // Second submit: the onBlur error is stale (PIN field still has it), but + // re-running validators on submit should clear it since type !== 'PIN' + await form.handleSubmit() + expect(onSubmit).toHaveBeenCalledTimes(1) + expect(onSubmitInvalid).toHaveBeenCalledTimes(1) // not called again + }) + it('should run all types of async validation on fields during submit', async () => { vi.useFakeTimers()