Skip to content
Merged
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
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