diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d30..d8d46a55c7 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2104,6 +2104,10 @@ export class FormApi< return } + // A value change or a new submission clears stale onSubmit/onServer + // errors. A blur without a change keeps them. + const clearsSubmitErrors = cause === 'change' || cause === 'submit' + /** * 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 @@ -2112,7 +2116,7 @@ export class FormApi< if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.errorMap?.[submitErrKey] && - cause !== 'submit' && + clearsSubmitErrors && !hasErrored ) { this.baseStore.setState((prev) => ({ @@ -2132,7 +2136,7 @@ export class FormApi< if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.errorMap?.[serverErrKey] && - cause !== 'server' && + clearsSubmitErrors && !hasErrored ) { this.baseStore.setState((prev) => ({ diff --git a/packages/form-core/src/FormGroupApi.ts b/packages/form-core/src/FormGroupApi.ts index 4790d5505c..0059edd5e2 100644 --- a/packages/form-core/src/FormGroupApi.ts +++ b/packages/form-core/src/FormGroupApi.ts @@ -1908,7 +1908,7 @@ export class FormGroupApi< if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.meta.errorMap?.[submitErrKey] && - cause !== 'submit' && + cause === 'change' && !hasErrored ) { this.setMeta((prev) => ({ diff --git a/packages/form-core/src/ValidationLogic.ts b/packages/form-core/src/ValidationLogic.ts index b4e8b0ac05..e137937941 100644 --- a/packages/form-core/src/ValidationLogic.ts +++ b/packages/form-core/src/ValidationLogic.ts @@ -194,9 +194,10 @@ export const defaultValidationLogic: ValidationLogicFn = (props) => { }) } case 'blur': { - // Run blur, server validation + // Run blur validation. Server errors are only cleared by a value + // change or a submit, not by a blur without a change. return props.runValidation({ - validators: [onBlurValidator, onServerValidator], + validators: [onBlurValidator], form: props.form, }) } diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c57bb044e8..e34502d075 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -2063,6 +2063,116 @@ describe('form api', () => { ).toBeUndefined() }) + it('should not clear form onSubmit errors 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).toEqual('first name is required') + + field.handleBlur() + expect(form.state.errorMap.onSubmit).toEqual('first name is required') + + field.handleChange('valid') + expect(form.state.errorMap.onSubmit).toBeUndefined() + }) + + it('should not clear form onServer errors on blur when the value did not change', () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + }) + form.mount() + const field = new FieldApi({ + form, + name: 'firstName', + }) + field.mount() + + form.setErrorMap({ onServer: 'server error' as never }) + expect(form.state.errorMap.onServer).toEqual('server error') + + field.handleBlur() + expect(form.state.errorMap.onServer).toEqual('server error') + + field.handleChange('valid') + expect(form.state.errorMap.onServer).toBeUndefined() + }) + + it('should not clear form onServer errors on blur when form validators are set', () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + validators: { + onChange: () => undefined, + }, + }) + form.mount() + const field = new FieldApi({ + form, + name: 'firstName', + }) + field.mount() + + form.setErrorMap({ onServer: 'server error' as never }) + + field.handleBlur() + expect(form.state.errorMap.onServer).toEqual('server error') + + field.handleChange('valid') + expect(form.state.errorMap.onServer).toBeUndefined() + }) + + it.each(['onServer', 'onSubmit'] as const)( + 'should clear form %s errors on a new submission without validators', + async (errorKey) => { + let attempts = 0 + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + onSubmit: ({ formApi }) => { + attempts++ + if (attempts === 1) { + formApi.setErrorMap({ [errorKey]: 'temporary failure' as never }) + } + }, + }) + form.mount() + const field = new FieldApi({ + form, + name: 'firstName', + }) + field.mount() + + await form.handleSubmit() + expect(attempts).toBe(1) + expect(form.state.errorMap[errorKey]).toEqual('temporary failure') + + field.handleBlur() + expect(form.state.errorMap[errorKey]).toEqual('temporary failure') + + await form.handleSubmit() + expect(attempts).toBe(2) + expect(form.state.errorMap[errorKey]).toBeUndefined() + }, + ) + it('should validate all fields consistently - field level onChange validators', async () => { const form = new FormApi({ defaultValues: {