Skip to content

Commit 2f8bf5a

Browse files
authored
feat: extend form.setErrorMap to spread errors in fields (#1489)
* feat: extend form.setErrorMap to spread errors in fields * test: check nonexistent fields * fix: set default value for TFormData
1 parent 5565233 commit 2f8bf5a

File tree

4 files changed

+151
-30
lines changed

4 files changed

+151
-30
lines changed

packages/form-core/src/FormApi.ts

+48-10
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ export type BaseFormState<
513513
/**
514514
* The error map for the form itself.
515515
*/
516-
errorMap: FormValidationErrorMap<
516+
errorMap: ValidationErrorMap<
517517
UnwrapFormValidateOrFn<TOnMount>,
518518
UnwrapFormValidateOrFn<TOnChange>,
519519
UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
@@ -2148,7 +2148,8 @@ export class FormApi<
21482148
* Updates the form's errorMap
21492149
*/
21502150
setErrorMap(
2151-
errorMap: ValidationErrorMap<
2151+
errorMap: FormValidationErrorMap<
2152+
TFormData,
21522153
UnwrapFormValidateOrFn<TOnMount>,
21532154
UnwrapFormValidateOrFn<TOnChange>,
21542155
UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
@@ -2159,13 +2160,50 @@ export class FormApi<
21592160
UnwrapFormAsyncValidateOrFn<TOnServer>
21602161
>,
21612162
) {
2162-
this.baseStore.setState((prev) => ({
2163-
...prev,
2164-
errorMap: {
2165-
...prev.errorMap,
2166-
...errorMap,
2167-
},
2168-
}))
2163+
batch(() => {
2164+
Object.entries(errorMap).forEach(([key, value]) => {
2165+
const errorMapKey = key as ValidationErrorMapKeys
2166+
2167+
if (isGlobalFormValidationError(value)) {
2168+
const { formError, fieldErrors } = normalizeError<TFormData>(value)
2169+
2170+
for (const fieldName of Object.keys(
2171+
this.fieldInfo,
2172+
) as DeepKeys<TFormData>[]) {
2173+
const fieldMeta = this.getFieldMeta(fieldName)
2174+
if (!fieldMeta) continue
2175+
2176+
this.setFieldMeta(fieldName, (prev) => ({
2177+
...prev,
2178+
errorMap: {
2179+
...prev.errorMap,
2180+
[errorMapKey]: fieldErrors?.[fieldName],
2181+
},
2182+
errorSourceMap: {
2183+
...prev.errorSourceMap,
2184+
[errorMapKey]: 'form',
2185+
},
2186+
}))
2187+
}
2188+
2189+
this.baseStore.setState((prev) => ({
2190+
...prev,
2191+
errorMap: {
2192+
...prev.errorMap,
2193+
[errorMapKey]: formError,
2194+
},
2195+
}))
2196+
} else {
2197+
this.baseStore.setState((prev) => ({
2198+
...prev,
2199+
errorMap: {
2200+
...prev.errorMap,
2201+
[errorMapKey]: value,
2202+
},
2203+
}))
2204+
}
2205+
})
2206+
})
21692207
}
21702208

21712209
/**
@@ -2183,7 +2221,7 @@ export class FormApi<
21832221
| UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>
21842222
| UnwrapFormAsyncValidateOrFn<TOnServer>
21852223
>
2186-
errorMap: FormValidationErrorMap<
2224+
errorMap: ValidationErrorMap<
21872225
UnwrapFormValidateOrFn<TOnMount>,
21882226
UnwrapFormValidateOrFn<TOnChange>,
21892227
UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,

packages/form-core/src/types.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export type ValidationErrorMapSource = {
5555
* @private
5656
*/
5757
export type FormValidationErrorMap<
58+
TFormData = unknown,
5859
TOnMountReturn = unknown,
5960
TOnChangeReturn = unknown,
6061
TOnChangeAsyncReturn = unknown,
@@ -64,10 +65,19 @@ export type FormValidationErrorMap<
6465
TOnSubmitAsyncReturn = unknown,
6566
TOnServerReturn = unknown,
6667
> = {
67-
onMount?: TOnMountReturn
68-
onChange?: TOnChangeReturn | TOnChangeAsyncReturn
69-
onBlur?: TOnBlurReturn | TOnBlurAsyncReturn
70-
onSubmit?: TOnSubmitReturn | TOnSubmitAsyncReturn
68+
onMount?: TOnMountReturn | GlobalFormValidationError<TFormData>
69+
onChange?:
70+
| TOnChangeReturn
71+
| TOnChangeAsyncReturn
72+
| GlobalFormValidationError<TFormData>
73+
onBlur?:
74+
| TOnBlurReturn
75+
| TOnBlurAsyncReturn
76+
| GlobalFormValidationError<TFormData>
77+
onSubmit?:
78+
| TOnSubmitReturn
79+
| TOnSubmitAsyncReturn
80+
| GlobalFormValidationError<TFormData>
7181
onServer?: TOnServerReturn
7282
}
7383

packages/form-core/tests/FormApi.spec.ts

+31
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,37 @@ describe('form api', () => {
22852285
expect(form.state.errorMap.onChange).toEqual('other validation error')
22862286
})
22872287

2288+
it('should spread errors in fields when setErrorMap receives a global form validation error', () => {
2289+
const form = new FormApi({
2290+
defaultValues: { name: '', interests: [] as { label: string }[] },
2291+
})
2292+
form.mount()
2293+
2294+
const field = new FieldApi({ form, name: 'name' })
2295+
field.mount()
2296+
2297+
const arrayElementField = new FieldApi({ form, name: 'interests[0].label' })
2298+
arrayElementField.mount()
2299+
2300+
form.setErrorMap({
2301+
onChange: {
2302+
form: 'global error',
2303+
fields: {
2304+
name: 'name is required',
2305+
'interests[0].label': 'label is required',
2306+
},
2307+
},
2308+
onBlur: 'Form Error' as never,
2309+
})
2310+
2311+
expect(form.state.errorMap.onChange).toEqual('global error')
2312+
expect(form.state.errorMap.onBlur).toEqual('Form Error')
2313+
expect(field.getMeta().errorMap.onChange).toEqual('name is required')
2314+
expect(arrayElementField.getMeta().errorMap.onChange).toEqual(
2315+
'label is required',
2316+
)
2317+
})
2318+
22882319
it("should set errors for the fields from the form's onSubmit validator", async () => {
22892320
const form = new FormApi({
22902321
defaultValues: {

packages/form-core/tests/FormApi.test-d.ts

+58-16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expectTypeOf, it } from 'vitest'
22
import { z } from 'zod'
33
import { FormApi } from '../src'
44
import type {
5+
GlobalFormValidationError,
56
StandardSchemaV1Issue,
67
ValidationError,
78
ValidationErrorMap,
@@ -118,11 +119,13 @@ it('should only have form-level error types returned from parseFieldValuesWithSc
118119
})
119120

120121
it("should allow setting manual errors according to the validator's return type", () => {
122+
type FormData = {
123+
firstName: string
124+
lastName: string
125+
}
126+
121127
const form = new FormApi({
122-
defaultValues: {
123-
firstName: '',
124-
lastName: '',
125-
},
128+
defaultValues: {} as FormData,
126129
validators: {
127130
onChange: () => ['onChange'] as const,
128131
onMount: () => 10 as const,
@@ -134,28 +137,67 @@ it("should allow setting manual errors according to the validator's return type"
134137
},
135138
})
136139

140+
form.setErrorMap({
141+
onMount: 10,
142+
onChange: ['onChange'],
143+
})
144+
137145
expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{
138-
onMount: 10 | undefined
139-
onChange: readonly ['onChange'] | 'onChangeAsync' | undefined
140-
onBlur: { onBlur: true; onBlurNumber: number } | 'onBlurAsync' | undefined
141-
onSubmit: 'onSubmit' | 'onSubmitAsync' | undefined
146+
onMount: 10 | undefined | GlobalFormValidationError<FormData>
147+
onChange:
148+
| readonly ['onChange']
149+
| 'onChangeAsync'
150+
| undefined
151+
| GlobalFormValidationError<FormData>
152+
onBlur:
153+
| { onBlur: true; onBlurNumber: number }
154+
| 'onBlurAsync'
155+
| undefined
156+
| GlobalFormValidationError<FormData>
157+
onSubmit:
158+
| 'onSubmit'
159+
| 'onSubmitAsync'
160+
| undefined
161+
| GlobalFormValidationError<FormData>
142162
onServer: undefined
143163
}>
144164
})
145165

146-
it('should not allow setting manual errors if no validator is specified', () => {
166+
it('should allow setting field errors from the global form error map', () => {
167+
type FormData = {
168+
firstName: string
169+
lastName: string
170+
}
171+
147172
const form = new FormApi({
148-
defaultValues: {
149-
firstName: '',
150-
lastName: '',
173+
defaultValues: {} as FormData,
174+
})
175+
176+
form.setErrorMap({
177+
onChange: {
178+
fields: {
179+
firstName: 'error',
180+
// @ts-expect-error
181+
nonExistentField: 'error',
182+
},
151183
},
152184
})
185+
})
186+
187+
it('should not allow setting manual errors if no validator is specified', () => {
188+
type FormData = {
189+
firstName: string
190+
lastName: string
191+
}
192+
const form = new FormApi({
193+
defaultValues: {} as FormData,
194+
})
153195

154196
expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{
155-
onMount: undefined
156-
onChange: undefined
157-
onBlur: undefined
158-
onSubmit: undefined
197+
onMount: undefined | GlobalFormValidationError<FormData>
198+
onChange: undefined | GlobalFormValidationError<FormData>
199+
onBlur: undefined | GlobalFormValidationError<FormData>
200+
onSubmit: undefined | GlobalFormValidationError<FormData>
159201
onServer: undefined
160202
}>
161203
})

0 commit comments

Comments
 (0)