1
- import type { DeepKeys , DeepValue , Updater } from './utils'
2
- import type { FormApi , ValidationError } from './FormApi'
1
+ import { type DeepKeys , type DeepValue , type Updater } from './utils'
2
+ import type { FormApi , ValidationError , ValidationErrorMap } from './FormApi'
3
3
import { Store } from '@tanstack/store'
4
4
5
- export type ValidationCause = 'change' | 'blur' | 'submit'
5
+ export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
6
6
7
7
type ValidateFn < TData , TFormData > = (
8
8
value : TData ,
@@ -52,8 +52,9 @@ export type FieldApiOptions<TData, TFormData> = FieldOptions<
52
52
53
53
export type FieldMeta = {
54
54
isTouched : boolean
55
- touchedError ?: ValidationError
56
- error ?: ValidationError
55
+ touchedErrors : ValidationError [ ]
56
+ errors : ValidationError [ ]
57
+ errorMap : ValidationErrorMap
57
58
isValidating : boolean
58
59
}
59
60
@@ -110,16 +111,19 @@ export class FieldApi<TData, TFormData> {
110
111
meta : this . _getMeta ( ) ?? {
111
112
isValidating : false ,
112
113
isTouched : false ,
114
+ touchedErrors : [ ] ,
115
+ errors : [ ] ,
116
+ errorMap : { } ,
113
117
...opts . defaultMeta ,
114
118
} ,
115
119
} ,
116
120
{
117
121
onUpdate : ( ) => {
118
122
const state = this . store . state
119
123
120
- state . meta . touchedError = state . meta . isTouched
121
- ? state . meta . error
122
- : undefined
124
+ state . meta . touchedErrors = state . meta . isTouched
125
+ ? state . meta . errors
126
+ : [ ]
123
127
124
128
this . prevState = state
125
129
this . state = state
@@ -203,6 +207,9 @@ export class FieldApi<TData, TFormData> {
203
207
( {
204
208
isValidating : false ,
205
209
isTouched : false ,
210
+ touchedErrors : [ ] ,
211
+ errors : [ ] ,
212
+ errorMap : { } ,
206
213
...this . options . defaultMeta ,
207
214
} as FieldMeta )
208
215
@@ -239,24 +246,27 @@ export class FieldApi<TData, TFormData> {
239
246
const { onChange, onBlur } = this . options
240
247
const validate =
241
248
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
242
-
243
249
if ( ! validate ) return
244
250
245
251
// Use the validationCount for all field instances to
246
252
// track freshness of the validation
247
253
const validationCount = ( this . getInfo ( ) . validationCount || 0 ) + 1
248
254
this . getInfo ( ) . validationCount = validationCount
249
255
const error = normalizeError ( validate ( value as never , this as never ) )
250
-
251
- if ( this . state . meta . error !== error ) {
256
+ const errorMapKey = getErrorMapKey ( cause )
257
+ if ( error && this . state . meta . errorMap [ errorMapKey ] !== error ) {
252
258
this . setMeta ( ( prev ) => ( {
253
259
...prev ,
254
- error,
260
+ errors : [ ...prev . errors , error ] ,
261
+ errorMap : {
262
+ ...prev . errorMap ,
263
+ [ getErrorMapKey ( cause ) ] : error ,
264
+ } ,
255
265
} ) )
256
266
}
257
267
258
- // If a sync error is encountered, cancel any async validation
259
- if ( this . state . meta . error ) {
268
+ // If a sync error is encountered for the errorMapKey (eg. onChange) , cancel any async validation
269
+ if ( this . state . meta . errorMap [ errorMapKey ] ) {
260
270
this . cancelValidateAsync ( )
261
271
}
262
272
}
@@ -293,9 +303,7 @@ export class FieldApi<TData, TFormData> {
293
303
: cause === 'submit'
294
304
? onSubmitAsync
295
305
: onBlurAsync
296
-
297
- if ( ! validate ) return
298
-
306
+ if ( ! validate ) return [ ]
299
307
const debounceMs =
300
308
cause === 'submit'
301
309
? 0
@@ -328,21 +336,25 @@ export class FieldApi<TData, TFormData> {
328
336
329
337
// Only kick off validation if this validation is the latest attempt
330
338
if ( checkLatest ( ) ) {
339
+ const prevErrors = this . getMeta ( ) . errors
331
340
try {
332
341
const rawError = await validate ( value as never , this as never )
333
-
334
342
if ( checkLatest ( ) ) {
335
343
const error = normalizeError ( rawError )
336
344
this . setMeta ( ( prev ) => ( {
337
345
...prev ,
338
346
isValidating : false ,
339
- error,
347
+ errors : [ ...prev . errors , error ] ,
348
+ errorMap : {
349
+ ...prev . errorMap ,
350
+ [ getErrorMapKey ( cause ) ] : error ,
351
+ } ,
340
352
} ) )
341
- this . getInfo ( ) . validationResolve ?.( error )
353
+ this . getInfo ( ) . validationResolve ?.( [ ... prevErrors , error ] )
342
354
}
343
355
} catch ( error ) {
344
356
if ( checkLatest ( ) ) {
345
- this . getInfo ( ) . validationReject ?.( error )
357
+ this . getInfo ( ) . validationReject ?.( [ ... prevErrors , error ] )
346
358
throw error
347
359
}
348
360
} finally {
@@ -354,26 +366,25 @@ export class FieldApi<TData, TFormData> {
354
366
}
355
367
356
368
// Always return the latest validation promise to the caller
357
- return this . getInfo ( ) . validationPromise
369
+ return this . getInfo ( ) . validationPromise ?? [ ]
358
370
}
359
371
360
372
validate = (
361
373
cause : ValidationCause ,
362
374
value ?: typeof this . _tdata ,
363
- ) : ValidationError | Promise < ValidationError > => {
375
+ ) : ValidationError [ ] | Promise < ValidationError [ ] > => {
364
376
// If the field is pristine and validatePristine is false, do not validate
365
- if ( ! this . state . meta . isTouched ) return
366
-
377
+ if ( ! this . state . meta . isTouched ) return [ ]
367
378
// Attempt to sync validate first
368
379
this . validateSync ( value , cause )
369
380
370
- // If there is an error, return it, do not attempt async validation
371
- if ( this . state . meta . error ) {
381
+ const errorMapKey = getErrorMapKey ( cause )
382
+ // If there is an error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
383
+ if ( this . getMeta ( ) . errorMap [ errorMapKey ] ) {
372
384
if ( ! this . options . asyncAlways ) {
373
- return this . state . meta . error
385
+ return this . state . meta . errors
374
386
}
375
387
}
376
-
377
388
// No error? Attempt async validation
378
389
return this . validateAsync ( value , cause )
379
390
}
@@ -403,3 +414,16 @@ function normalizeError(rawError?: ValidationError) {
403
414
404
415
return undefined
405
416
}
417
+
418
+ function getErrorMapKey ( cause : ValidationCause ) {
419
+ switch ( cause ) {
420
+ case 'submit' :
421
+ return 'onSubmit'
422
+ case 'change' :
423
+ return 'onChange'
424
+ case 'blur' :
425
+ return 'onBlur'
426
+ case 'mount' :
427
+ return 'onMount'
428
+ }
429
+ }
0 commit comments