Skip to content

Commit

Permalink
fix(form-core): async form validator runs only if sync field & form p…
Browse files Browse the repository at this point in the history
…assed (#1029)

* fix(form-core): make sure async form validator is called only if sync field & form passed

* docs: update references

* tests: add test for asyncAlways in the field
  • Loading branch information
Balastrong authored Nov 19, 2024
1 parent 04238b1 commit 362ce10
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 21 deletions.
6 changes: 3 additions & 3 deletions docs/reference/classes/fieldapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Handles the blur event.

#### Defined in

[packages/form-core/src/FieldApi.ts:1021](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1021)
[packages/form-core/src/FieldApi.ts:1010](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1010)

***

Expand All @@ -225,7 +225,7 @@ Handles the change event.

#### Defined in

[packages/form-core/src/FieldApi.ts:1014](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1014)
[packages/form-core/src/FieldApi.ts:1003](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1003)

***

Expand Down Expand Up @@ -404,7 +404,7 @@ Updates the field's errorMap
#### Defined in
[packages/form-core/src/FieldApi.ts:1036](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1036)
[packages/form-core/src/FieldApi.ts:1025](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1025)
***
Expand Down
25 changes: 7 additions & 18 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,33 +978,22 @@ export class FieldApi<
// If the field is pristine, do not validate
if (!this.state.meta.isTouched) return []

let validationErrorFromForm: ValidationErrorMap = {}
let formValidationResultPromise: Promise<
FieldsErrorMapFromValidator<TParentData>
> = Promise.resolve({})

try {
const formValidationResult = this.form.validate(cause)
if (formValidationResult instanceof Promise) {
formValidationResultPromise = formValidationResult
} else {
const fieldErrorFromForm = formValidationResult[this.name]
if (fieldErrorFromForm) {
validationErrorFromForm = fieldErrorFromForm
}
}
} catch (_) {}

// Attempt to sync validate first
const { hasErrored } = this.validateSync(cause, validationErrorFromForm)
const { fieldsErrorMap } = this.form.validateSync(cause)
const { hasErrored } = this.validateSync(
cause,
fieldsErrorMap[this.name] ?? {},
)

if (hasErrored && !this.options.asyncAlways) {
this.getInfo().validationMetaMap[
getErrorMapKey(cause)
]?.lastAbortController.abort()
return this.state.meta.errors
}

// No error? Attempt async validation
const formValidationResultPromise = this.form.validateAsync(cause)
return this.validateAsync(cause, formValidationResultPromise)
}

Expand Down
117 changes: 117 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,123 @@ describe('form api', () => {
expect(form.state.errors).toStrictEqual([])
})

it('should run validators in order form sync -> field sync -> form async -> field async', async () => {
const order: string[] = []
const formAsyncChange = vi.fn().mockImplementation(async () => {
order.push('formAsyncChange')
await sleep(1000)
})
const formSyncChange = vi.fn().mockImplementation(() => {
order.push('formSyncChange')
})
const fieldAsyncChange = vi.fn().mockImplementation(async () => {
order.push('fieldAsyncChange')
await sleep(1000)
})
const fieldSyncChange = vi.fn().mockImplementation(() => {
order.push('fieldSyncChange')
})

const form = new FormApi({
defaultValues: {
firstName: '',
},
validators: {
onChange: formSyncChange,
onChangeAsync: formAsyncChange,
},
})

const firstNameField = new FieldApi({
form,
name: 'firstName',
validators: {
onChange: fieldSyncChange,
onChangeAsync: fieldAsyncChange,
},
})

form.mount()
firstNameField.mount()

firstNameField.handleChange('something')
await vi.runAllTimersAsync()

expect(order).toStrictEqual([
'formSyncChange',
'fieldSyncChange',
'formAsyncChange',
'fieldAsyncChange',
])
})

it('should not run form async validator if field sync has errored', async () => {
const formAsyncChange = vi.fn()
const formSyncChange = vi.fn()

const form = new FormApi({
defaultValues: {
firstName: '',
},
validators: {
onChange: formSyncChange,
onChangeAsync: formAsyncChange,
},
})

const firstNameField = new FieldApi({
form,
name: 'firstName',
validators: {
onChange: ({ value }) => (value.length > 0 ? undefined : 'field error'),
},
})

form.mount()
firstNameField.mount()

firstNameField.handleChange('')
await vi.runAllTimersAsync()

expect(formSyncChange).toHaveBeenCalled()
expect(firstNameField.state.meta.errorMap.onChange).toBe('field error')
expect(formAsyncChange).not.toHaveBeenCalled()
})

it('runs form async validator if field sync has errored and asyncAlways is true', async () => {
const formAsyncChange = vi.fn()
const formSyncChange = vi.fn()

const form = new FormApi({
defaultValues: {
firstName: '',
},
validators: {
onChange: formSyncChange,
onChangeAsync: formAsyncChange,
},
})

const firstNameField = new FieldApi({
form,
name: 'firstName',
asyncAlways: true,
validators: {
onChange: ({ value }) => (value.length > 0 ? undefined : 'field error'),
},
})

form.mount()
firstNameField.mount()

firstNameField.handleChange('')
await vi.runAllTimersAsync()

expect(formSyncChange).toHaveBeenCalled()
expect(firstNameField.state.meta.errorMap.onChange).toBe('field error')
expect(formAsyncChange).toHaveBeenCalled()
})

it("should set errors for the fields from the form's onChange validator", async () => {
const form = new FormApi({
defaultValues: {
Expand Down

0 comments on commit 362ce10

Please sign in to comment.