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
58 changes: 48 additions & 10 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ export type BaseFormState<
/**
* The error map for the form itself.
*/
errorMap: FormValidationErrorMap<
errorMap: ValidationErrorMap<
UnwrapFormValidateOrFn<TOnMount>,
UnwrapFormValidateOrFn<TOnChange>,
UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
Expand Down Expand Up @@ -2122,7 +2122,8 @@ export class FormApi<
* Updates the form's errorMap
*/
setErrorMap(
errorMap: ValidationErrorMap<
errorMap: FormValidationErrorMap<
TFormData,
UnwrapFormValidateOrFn<TOnMount>,
UnwrapFormValidateOrFn<TOnChange>,
UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
Expand All @@ -2133,13 +2134,50 @@ export class FormApi<
UnwrapFormAsyncValidateOrFn<TOnServer>
>,
) {
this.baseStore.setState((prev) => ({
...prev,
errorMap: {
...prev.errorMap,
...errorMap,
},
}))
batch(() => {
Object.entries(errorMap).forEach(([key, value]) => {
const errorMapKey = key as ValidationErrorMapKeys

if (isGlobalFormValidationError(value)) {
const { formError, fieldErrors } = normalizeError<TFormData>(value)

for (const fieldName of Object.keys(
this.fieldInfo,
) as DeepKeys<TFormData>[]) {
const fieldMeta = this.getFieldMeta(fieldName)
if (!fieldMeta) continue

this.setFieldMeta(fieldName, (prev) => ({
...prev,
errorMap: {
...prev.errorMap,
[errorMapKey]: fieldErrors?.[fieldName],
},
errorSourceMap: {
...prev.errorSourceMap,
[errorMapKey]: 'form',
},
}))
}

this.baseStore.setState((prev) => ({
...prev,
errorMap: {
...prev.errorMap,
[errorMapKey]: formError,
},
}))
} else {
this.baseStore.setState((prev) => ({
...prev,
errorMap: {
...prev.errorMap,
[errorMapKey]: value,
},
}))
}
})
})
}

/**
Expand All @@ -2157,7 +2195,7 @@ export class FormApi<
| UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>
| UnwrapFormAsyncValidateOrFn<TOnServer>
>
errorMap: FormValidationErrorMap<
errorMap: ValidationErrorMap<
UnwrapFormValidateOrFn<TOnMount>,
UnwrapFormValidateOrFn<TOnChange>,
UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
Expand Down
18 changes: 14 additions & 4 deletions packages/form-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type ValidationErrorMapSource = {
* @private
*/
export type FormValidationErrorMap<
TFormData = unknown,
TOnMountReturn = unknown,
TOnChangeReturn = unknown,
TOnChangeAsyncReturn = unknown,
Expand All @@ -64,10 +65,19 @@ export type FormValidationErrorMap<
TOnSubmitAsyncReturn = unknown,
TOnServerReturn = unknown,
> = {
onMount?: TOnMountReturn
onChange?: TOnChangeReturn | TOnChangeAsyncReturn
onBlur?: TOnBlurReturn | TOnBlurAsyncReturn
onSubmit?: TOnSubmitReturn | TOnSubmitAsyncReturn
onMount?: TOnMountReturn | GlobalFormValidationError<TFormData>
onChange?:
| TOnChangeReturn
| TOnChangeAsyncReturn
| GlobalFormValidationError<TFormData>
onBlur?:
| TOnBlurReturn
| TOnBlurAsyncReturn
| GlobalFormValidationError<TFormData>
onSubmit?:
| TOnSubmitReturn
| TOnSubmitAsyncReturn
| GlobalFormValidationError<TFormData>
onServer?: TOnServerReturn
}

Expand Down
31 changes: 31 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,37 @@ describe('form api', () => {
expect(form.state.errorMap.onChange).toEqual('other validation error')
})

it('should spread errors in fields when setErrorMap receives a global form validation error', () => {
const form = new FormApi({
defaultValues: { name: '', interests: [] as { label: string }[] },
})
form.mount()

const field = new FieldApi({ form, name: 'name' })
field.mount()

const arrayElementField = new FieldApi({ form, name: 'interests[0].label' })
arrayElementField.mount()

form.setErrorMap({
onChange: {
form: 'global error',
fields: {
name: 'name is required',
'interests[0].label': 'label is required',
},
},
onBlur: 'Form Error' as never,
})

expect(form.state.errorMap.onChange).toEqual('global error')
expect(form.state.errorMap.onBlur).toEqual('Form Error')
expect(field.getMeta().errorMap.onChange).toEqual('name is required')
expect(arrayElementField.getMeta().errorMap.onChange).toEqual(
'label is required',
)
})

it("should set errors for the fields from the form's onSubmit validator", async () => {
const form = new FormApi({
defaultValues: {
Expand Down
74 changes: 58 additions & 16 deletions packages/form-core/tests/FormApi.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expectTypeOf, it } from 'vitest'
import { z } from 'zod'
import { FormApi } from '../src'
import type {
GlobalFormValidationError,
StandardSchemaV1Issue,
ValidationError,
ValidationErrorMap,
Expand Down Expand Up @@ -118,11 +119,13 @@ it('should only have form-level error types returned from parseFieldValuesWithSc
})

it("should allow setting manual errors according to the validator's return type", () => {
type FormData = {
firstName: string
lastName: string
}

const form = new FormApi({
defaultValues: {
firstName: '',
lastName: '',
},
defaultValues: {} as FormData,
validators: {
onChange: () => ['onChange'] as const,
onMount: () => 10 as const,
Expand All @@ -134,28 +137,67 @@ it("should allow setting manual errors according to the validator's return type"
},
})

form.setErrorMap({
onMount: 10,
onChange: ['onChange'],
})

expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{
onMount: 10 | undefined
onChange: readonly ['onChange'] | 'onChangeAsync' | undefined
onBlur: { onBlur: true; onBlurNumber: number } | 'onBlurAsync' | undefined
onSubmit: 'onSubmit' | 'onSubmitAsync' | undefined
onMount: 10 | undefined | GlobalFormValidationError<FormData>
onChange:
| readonly ['onChange']
| 'onChangeAsync'
| undefined
| GlobalFormValidationError<FormData>
onBlur:
| { onBlur: true; onBlurNumber: number }
| 'onBlurAsync'
| undefined
| GlobalFormValidationError<FormData>
onSubmit:
| 'onSubmit'
| 'onSubmitAsync'
| undefined
| GlobalFormValidationError<FormData>
onServer: undefined
}>
})

it('should not allow setting manual errors if no validator is specified', () => {
it('should allow setting field errors from the global form error map', () => {
type FormData = {
firstName: string
lastName: string
}

const form = new FormApi({
defaultValues: {
firstName: '',
lastName: '',
defaultValues: {} as FormData,
})

form.setErrorMap({
onChange: {
fields: {
firstName: 'error',
// @ts-expect-error
nonExistentField: 'error',
},
},
})
})

it('should not allow setting manual errors if no validator is specified', () => {
type FormData = {
firstName: string
lastName: string
}
const form = new FormApi({
defaultValues: {} as FormData,
})

expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{
onMount: undefined
onChange: undefined
onBlur: undefined
onSubmit: undefined
onMount: undefined | GlobalFormValidationError<FormData>
onChange: undefined | GlobalFormValidationError<FormData>
onBlur: undefined | GlobalFormValidationError<FormData>
onSubmit: undefined | GlobalFormValidationError<FormData>
onServer: undefined
}>
})