Skip to content
Open
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
8 changes: 6 additions & 2 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) => ({
Expand All @@ -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) => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/form-core/src/FormGroupApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
5 changes: 3 additions & 2 deletions packages/form-core/src/ValidationLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
Expand Down
110 changes: 110 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down