diff --git a/.changeset/form-clear-onsubmit-error-on-change.md b/.changeset/form-clear-onsubmit-error-on-change.md new file mode 100644 index 0000000000..514ed9cbeb --- /dev/null +++ b/.changeset/form-clear-onsubmit-error-on-change.md @@ -0,0 +1,12 @@ +--- +'@tanstack/form-core': patch +--- + +fix(form-core): only clear a form-level onSubmit error on value change + +`FormApi` cleared a stale form-level `onSubmit` error on any non-`submit` +validation cause (`cause !== 'submit'`), so a `blur`, `mount`, or `dynamic` +revalidation dropped the error even though the user never edited the field. The +clear now only happens on `cause === 'change'`, matching the documented intent +("clear the error as soon as the user enters a valid value") and the field-level +fix in #2211. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d30..2a99077861 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2106,13 +2106,16 @@ export class FormApi< /** * when we have an error for onSubmit in the state, we want - * to clear the error as soon as the user enters a valid value in the field + * to clear the error as soon as the user enters a valid value in the field. + * This must only happen on a value `change` - clearing it on `blur` (or any + * other non-value cause like `mount`, `server` or `dynamic`) would wrongly + * drop the submit error when the field is revalidated without being edited. */ const submitErrKey = getErrorMapKey('submit') if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.errorMap?.[submitErrKey] && - cause !== 'submit' && + cause === 'change' && !hasErrored ) { this.baseStore.setState((prev) => ({ diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c57bb044e8..c0cc62bc0c 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -2162,6 +2162,63 @@ describe('form api', () => { expect(form.state.errors).toStrictEqual(['first name is required']) }) + it('should not clear the form-level onSubmit error on blur when the value did not change', async () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + validators: { + onSubmit: ({ value }) => + value.firstName.length > 0 ? undefined : 'first name is required', + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'firstName', + }) + + field.mount() + + await form.handleSubmit() + expect(form.state.errorMap.onSubmit).toBe('first name is required') + + // Blurring the field without changing its value must keep the submit error: + // `blur` is not a value change, so the stale onSubmit error should remain. + field.handleBlur() + expect(form.state.errorMap.onSubmit).toBe('first name is required') + }) + + it('should clear the form-level onSubmit error once a valid value is entered', async () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + validators: { + onSubmit: ({ value }) => + value.firstName.length > 0 ? undefined : 'first name is required', + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'firstName', + }) + + field.mount() + + await form.handleSubmit() + expect(form.state.errorMap.onSubmit).toBe('first name is required') + + // Entering a valid value clears the stale submit error. + field.handleChange('John') + expect(form.state.errorMap.onSubmit).toBeUndefined() + }) + it('should run onChange validation during submit', async () => { const form = new FormApi({ defaultValues: {