@tanstack/form-core 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/build/legacy/FieldApi.cjs +98 -125
  2. package/build/legacy/FieldApi.cjs.map +1 -1
  3. package/build/legacy/FieldApi.d.cts +1 -2
  4. package/build/legacy/FieldApi.d.ts +1 -2
  5. package/build/legacy/FieldApi.js +98 -125
  6. package/build/legacy/FieldApi.js.map +1 -1
  7. package/build/legacy/FormApi.cjs +128 -121
  8. package/build/legacy/FormApi.cjs.map +1 -1
  9. package/build/legacy/FormApi.d.cts +1 -2
  10. package/build/legacy/FormApi.d.ts +1 -2
  11. package/build/legacy/FormApi.js +130 -121
  12. package/build/legacy/FormApi.js.map +1 -1
  13. package/build/legacy/index.d.cts +163 -74
  14. package/build/legacy/index.d.ts +163 -74
  15. package/build/legacy/types.cjs.map +1 -1
  16. package/build/legacy/types.d.cts +12 -3
  17. package/build/legacy/types.d.ts +12 -3
  18. package/build/legacy/utils.cjs +55 -0
  19. package/build/legacy/utils.cjs.map +1 -1
  20. package/build/legacy/utils.d.cts +3 -37
  21. package/build/legacy/utils.d.ts +3 -37
  22. package/build/legacy/utils.js +53 -0
  23. package/build/legacy/utils.js.map +1 -1
  24. package/build/modern/FieldApi.cjs +98 -123
  25. package/build/modern/FieldApi.cjs.map +1 -1
  26. package/build/modern/FieldApi.d.cts +1 -2
  27. package/build/modern/FieldApi.d.ts +1 -2
  28. package/build/modern/FieldApi.js +98 -123
  29. package/build/modern/FieldApi.js.map +1 -1
  30. package/build/modern/FormApi.cjs +128 -120
  31. package/build/modern/FormApi.cjs.map +1 -1
  32. package/build/modern/FormApi.d.cts +1 -2
  33. package/build/modern/FormApi.d.ts +1 -2
  34. package/build/modern/FormApi.js +130 -120
  35. package/build/modern/FormApi.js.map +1 -1
  36. package/build/modern/index.d.cts +163 -74
  37. package/build/modern/index.d.ts +163 -74
  38. package/build/modern/types.cjs.map +1 -1
  39. package/build/modern/types.d.cts +12 -3
  40. package/build/modern/types.d.ts +12 -3
  41. package/build/modern/utils.cjs +55 -0
  42. package/build/modern/utils.cjs.map +1 -1
  43. package/build/modern/utils.d.cts +3 -37
  44. package/build/modern/utils.d.ts +3 -37
  45. package/build/modern/utils.js +53 -0
  46. package/build/modern/utils.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/FieldApi.ts +315 -241
  49. package/src/FormApi.ts +263 -213
  50. package/src/tests/FieldApi.spec.ts +135 -48
  51. package/src/tests/FieldApi.test-d.ts +10 -6
  52. package/src/tests/FormApi.spec.ts +192 -61
  53. package/src/tests/utils.ts +1 -1
  54. package/src/types.ts +10 -2
  55. package/src/utils.ts +106 -0
package/src/FieldApi.ts CHANGED
@@ -1,115 +1,203 @@
1
1
  import { Store } from '@tanstack/store'
2
- import type { FormApi, ValidationErrorMap } from './FormApi'
3
- import type { ValidationError, Validator } from './types'
2
+ import type { FormApi } from './FormApi'
3
+ import type {
4
+ ValidationCause,
5
+ ValidationError,
6
+ ValidationErrorMap,
7
+ Validator,
8
+ } from './types'
4
9
  import type { DeepKeys, DeepValue, Updater } from './utils'
10
+ import { getAsyncValidatorArray, getSyncValidatorArray } from './utils'
5
11
 
6
- export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
7
-
8
- type ValidateFn<
12
+ export type FieldValidateFn<
9
13
  TParentData,
10
14
  TName extends DeepKeys<TParentData>,
11
- ValidatorType,
15
+ TFieldValidator extends
16
+ | Validator<DeepValue<TParentData, TName>, unknown>
17
+ | undefined = undefined,
18
+ TFormValidator extends
19
+ | Validator<TParentData, unknown>
20
+ | undefined = undefined,
12
21
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
13
- > = (
14
- value: TData,
15
- fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>,
16
- ) => ValidationError
22
+ > = (props: {
23
+ value: TData
24
+ fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
25
+ }) => ValidationError
17
26
 
18
- type ValidateOrFn<
27
+ export type FieldValidateOrFn<
19
28
  TParentData,
20
29
  TName extends DeepKeys<TParentData>,
21
- ValidatorType,
22
- FormValidator,
30
+ TFieldValidator extends
31
+ | Validator<DeepValue<TParentData, TName>, unknown>
32
+ | undefined = undefined,
33
+ TFormValidator extends
34
+ | Validator<TParentData, unknown>
35
+ | undefined = undefined,
23
36
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
24
- > = ValidatorType extends Validator<TData>
37
+ > = TFieldValidator extends Validator<TData, infer TFN>
25
38
  ?
26
- | Parameters<ReturnType<ValidatorType>['validate']>[1]
27
- | ValidateFn<TParentData, TName, ValidatorType, TData>
28
- : FormValidator extends Validator<TData>
39
+ | TFN
40
+ | FieldValidateFn<
41
+ TParentData,
42
+ TName,
43
+ TFieldValidator,
44
+ TFormValidator,
45
+ TData
46
+ >
47
+ : TFormValidator extends Validator<TParentData, infer FFN>
29
48
  ?
30
- | Parameters<ReturnType<FormValidator>['validate']>[1]
31
- | ValidateFn<TParentData, TName, ValidatorType, TData>
32
- : ValidateFn<TParentData, TName, ValidatorType, TData>
33
-
34
- type ValidateAsyncFn<
49
+ | FFN
50
+ | FieldValidateFn<
51
+ TParentData,
52
+ TName,
53
+ TFieldValidator,
54
+ TFormValidator,
55
+ TData
56
+ >
57
+ : FieldValidateFn<TParentData, TName, TFieldValidator, TFormValidator, TData>
58
+
59
+ export type FieldValidateAsyncFn<
35
60
  TParentData,
36
61
  TName extends DeepKeys<TParentData>,
37
- ValidatorType,
62
+ TFieldValidator extends
63
+ | Validator<DeepValue<TParentData, TName>, unknown>
64
+ | undefined = undefined,
65
+ TFormValidator extends
66
+ | Validator<TParentData, unknown>
67
+ | undefined = undefined,
38
68
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
39
- > = (
40
- value: TData,
41
- fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>,
42
- ) => ValidationError | Promise<ValidationError>
69
+ > = (options: {
70
+ value: TData
71
+ fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
72
+ signal: AbortSignal
73
+ }) => ValidationError | Promise<ValidationError>
43
74
 
44
- type AsyncValidateOrFn<
75
+ export type FieldAsyncValidateOrFn<
45
76
  TParentData,
46
77
  TName extends DeepKeys<TParentData>,
47
- ValidatorType,
48
- FormValidator,
78
+ TFieldValidator extends
79
+ | Validator<DeepValue<TParentData, TName>, unknown>
80
+ | undefined = undefined,
81
+ TFormValidator extends
82
+ | Validator<TParentData, unknown>
83
+ | undefined = undefined,
49
84
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
50
- > = ValidatorType extends Validator<TData>
85
+ > = TFieldValidator extends Validator<TData, infer TFN>
51
86
  ?
52
- | Parameters<ReturnType<ValidatorType>['validate']>[1]
53
- | ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
54
- : FormValidator extends Validator<TData>
87
+ | TFN
88
+ | FieldValidateAsyncFn<
89
+ TParentData,
90
+ TName,
91
+ TFieldValidator,
92
+ TFormValidator,
93
+ TData
94
+ >
95
+ : TFormValidator extends Validator<TParentData, infer FFN>
55
96
  ?
56
- | Parameters<ReturnType<FormValidator>['validate']>[1]
57
- | ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
58
- : ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
97
+ | FFN
98
+ | FieldValidateAsyncFn<
99
+ TParentData,
100
+ TName,
101
+ TFieldValidator,
102
+ TFormValidator,
103
+ TData
104
+ >
105
+ : FieldValidateAsyncFn<
106
+ TParentData,
107
+ TName,
108
+ TFieldValidator,
109
+ TFormValidator,
110
+ TData
111
+ >
59
112
 
60
- export interface FieldOptions<
113
+ export interface FieldValidators<
61
114
  TParentData,
62
115
  TName extends DeepKeys<TParentData>,
63
- ValidatorType,
64
- FormValidator,
116
+ TFieldValidator extends
117
+ | Validator<DeepValue<TParentData, TName>, unknown>
118
+ | undefined = undefined,
119
+ TFormValidator extends
120
+ | Validator<TParentData, unknown>
121
+ | undefined = undefined,
65
122
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
66
123
  > {
67
- name: TName
68
- index?: TData extends any[] ? number : never
69
- defaultValue?: TData
70
- asyncDebounceMs?: number
71
- asyncAlways?: boolean
72
- preserveValue?: boolean
73
- validator?: ValidatorType
74
- onMount?: (
75
- formApi: FieldApi<TParentData, TName, ValidatorType, TData>,
76
- ) => void
77
- onChange?: ValidateOrFn<
124
+ onMount?: FieldValidateOrFn<
78
125
  TParentData,
79
126
  TName,
80
- ValidatorType,
81
- FormValidator,
127
+ TFieldValidator,
128
+ TFormValidator,
82
129
  TData
83
130
  >
84
- onChangeAsync?: AsyncValidateOrFn<
131
+ onChange?: FieldValidateOrFn<
85
132
  TParentData,
86
133
  TName,
87
- ValidatorType,
88
- FormValidator,
134
+ TFieldValidator,
135
+ TFormValidator,
136
+ TData
137
+ >
138
+ onChangeAsync?: FieldAsyncValidateOrFn<
139
+ TParentData,
140
+ TName,
141
+ TFieldValidator,
142
+ TFormValidator,
89
143
  TData
90
144
  >
91
145
  onChangeAsyncDebounceMs?: number
92
- onBlur?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>
93
- onBlurAsync?: AsyncValidateOrFn<
146
+ onBlur?: FieldValidateOrFn<
94
147
  TParentData,
95
148
  TName,
96
- ValidatorType,
97
- FormValidator,
149
+ TFieldValidator,
150
+ TFormValidator,
151
+ TData
152
+ >
153
+ onBlurAsync?: FieldAsyncValidateOrFn<
154
+ TParentData,
155
+ TName,
156
+ TFieldValidator,
157
+ TFormValidator,
98
158
  TData
99
159
  >
100
160
  onBlurAsyncDebounceMs?: number
101
- onSubmit?: ValidateOrFn<
161
+ onSubmit?: FieldValidateOrFn<
102
162
  TParentData,
103
163
  TName,
104
- ValidatorType,
105
- FormValidator,
164
+ TFieldValidator,
165
+ TFormValidator,
106
166
  TData
107
167
  >
108
- onSubmitAsync?: AsyncValidateOrFn<
168
+ onSubmitAsync?: FieldAsyncValidateOrFn<
109
169
  TParentData,
110
170
  TName,
111
- ValidatorType,
112
- FormValidator,
171
+ TFieldValidator,
172
+ TFormValidator,
173
+ TData
174
+ >
175
+ onSubmitAsyncDebounceMs?: number
176
+ }
177
+
178
+ export interface FieldOptions<
179
+ TParentData,
180
+ TName extends DeepKeys<TParentData>,
181
+ TFieldValidator extends
182
+ | Validator<DeepValue<TParentData, TName>, unknown>
183
+ | undefined = undefined,
184
+ TFormValidator extends
185
+ | Validator<TParentData, unknown>
186
+ | undefined = undefined,
187
+ TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
188
+ > {
189
+ name: TName
190
+ index?: TData extends any[] ? number : never
191
+ defaultValue?: TData
192
+ asyncDebounceMs?: number
193
+ asyncAlways?: boolean
194
+ preserveValue?: boolean
195
+ validatorAdapter?: TFieldValidator
196
+ validators?: FieldValidators<
197
+ TParentData,
198
+ TName,
199
+ TFieldValidator,
200
+ TFormValidator,
113
201
  TData
114
202
  >
115
203
  defaultMeta?: Partial<FieldMeta>
@@ -118,17 +206,21 @@ export interface FieldOptions<
118
206
  export interface FieldApiOptions<
119
207
  TParentData,
120
208
  TName extends DeepKeys<TParentData>,
121
- ValidatorType,
122
- FormValidator,
209
+ TFieldValidator extends
210
+ | Validator<DeepValue<TParentData, TName>, unknown>
211
+ | undefined = undefined,
212
+ TFormValidator extends
213
+ | Validator<TParentData, unknown>
214
+ | undefined = undefined,
123
215
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
124
216
  > extends FieldOptions<
125
217
  TParentData,
126
218
  TName,
127
- ValidatorType,
128
- FormValidator,
219
+ TFieldValidator,
220
+ TFormValidator,
129
221
  TData
130
222
  > {
131
- form: FormApi<TParentData, FormValidator>
223
+ form: FormApi<TParentData, TFormValidator>
132
224
  }
133
225
 
134
226
  export type FieldMeta = {
@@ -153,14 +245,30 @@ export type ResolveName<TParentData> = unknown extends TParentData
153
245
  export class FieldApi<
154
246
  TParentData,
155
247
  TName extends DeepKeys<TParentData>,
156
- ValidatorType,
157
- FormValidator,
248
+ TFieldValidator extends
249
+ | Validator<DeepValue<TParentData, TName>, unknown>
250
+ | undefined = undefined,
251
+ TFormValidator extends
252
+ | Validator<TParentData, unknown>
253
+ | undefined = undefined,
158
254
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
159
255
  > {
160
256
  uid: number
161
- form: FieldApiOptions<TParentData, TName, ValidatorType, TData>['form']
257
+ form: FieldApiOptions<
258
+ TParentData,
259
+ TName,
260
+ TFieldValidator,
261
+ TFormValidator,
262
+ TData
263
+ >['form']
162
264
  name!: DeepKeys<TParentData>
163
- options: FieldApiOptions<TParentData, TName, ValidatorType, TData> = {} as any
265
+ options: FieldApiOptions<
266
+ TParentData,
267
+ TName,
268
+ TFieldValidator,
269
+ TFormValidator,
270
+ TData
271
+ > = {} as any
164
272
  store!: Store<FieldState<TData>>
165
273
  state!: FieldState<TData>
166
274
  prevState!: FieldState<TData>
@@ -169,8 +277,8 @@ export class FieldApi<
169
277
  opts: FieldApiOptions<
170
278
  TParentData,
171
279
  TName,
172
- ValidatorType,
173
- FormValidator,
280
+ TFieldValidator,
281
+ TFormValidator,
174
282
  TData
175
283
  >,
176
284
  ) {
@@ -224,6 +332,29 @@ export class FieldApi<
224
332
  this.options = opts as never
225
333
  }
226
334
 
335
+ runValidator<
336
+ TValue extends { value: TData; fieldApi: FieldApi<any, any, any, any> },
337
+ TType extends 'validate' | 'validateAsync',
338
+ >(props: {
339
+ validate: TType extends 'validate'
340
+ ? FieldValidateOrFn<any, any, any, any>
341
+ : FieldAsyncValidateOrFn<any, any, any, any>
342
+ value: TValue
343
+ type: TType
344
+ }): ReturnType<ReturnType<Validator<any>>[TType]> {
345
+ const adapters = [
346
+ this.form.options.validatorAdapter,
347
+ this.options.validatorAdapter,
348
+ ] as const
349
+ for (const adapter of adapters) {
350
+ if (adapter && typeof props.validate !== 'function') {
351
+ return adapter()[props.type](props.value, props.validate) as never
352
+ }
353
+ }
354
+
355
+ return (props.validate as FieldValidateFn<any, any>)(props.value) as never
356
+ }
357
+
227
358
  mount = () => {
228
359
  const info = this.getInfo()
229
360
  info.instances[this.uid] = this as never
@@ -243,7 +374,25 @@ export class FieldApi<
243
374
  })
244
375
 
245
376
  this.update(this.options as never)
246
- this.options.onMount?.(this as never)
377
+ const { onMount } = this.options.validators || {}
378
+
379
+ if (onMount) {
380
+ const error = this.runValidator({
381
+ validate: onMount,
382
+ value: {
383
+ value: this.state.value,
384
+ fieldApi: this,
385
+ },
386
+ type: 'validate',
387
+ })
388
+ if (error) {
389
+ this.setMeta((prev) => ({
390
+ ...prev,
391
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
392
+ errorMap: { ...prev?.errorMap, onMount: error },
393
+ }))
394
+ }
395
+ }
247
396
 
248
397
  return () => {
249
398
  const preserveValue = this.options.preserveValue
@@ -260,7 +409,13 @@ export class FieldApi<
260
409
  }
261
410
 
262
411
  update = (
263
- opts: FieldApiOptions<TParentData, TName, ValidatorType, TData>,
412
+ opts: FieldApiOptions<
413
+ TParentData,
414
+ TName,
415
+ TFieldValidator,
416
+ TFormValidator,
417
+ TData
418
+ >,
264
419
  ) => {
265
420
  // Default Value
266
421
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -330,60 +485,34 @@ export class FieldApi<
330
485
  TSubData extends DeepValue<TData, TSubName> = DeepValue<TData, TSubName>,
331
486
  >(
332
487
  name: TSubName,
333
- ): FieldApi<TData, TSubName, ValidatorType, TSubData> =>
488
+ ): FieldApi<
489
+ TData,
490
+ TSubName,
491
+ Validator<TSubData, unknown> | undefined,
492
+ Validator<TData, unknown> | undefined,
493
+ TSubData
494
+ > =>
334
495
  new FieldApi({
335
496
  name: `${this.name}.${name}` as never,
336
497
  form: this.form,
337
498
  }) as any
338
499
 
339
500
  validateSync = (value = this.state.value, cause: ValidationCause) => {
340
- const { onChange, onBlur, onSubmit } = this.options
341
-
342
- const validates =
343
- // https://github.com/TanStack/form/issues/490
344
- cause === 'submit'
345
- ? ([
346
- { cause: 'change', validate: onChange },
347
- { cause: 'blur', validate: onBlur },
348
- { cause: 'submit', validate: onSubmit },
349
- ] as const)
350
- : cause === 'change'
351
- ? ([{ cause: 'change', validate: onChange }] as const)
352
- : ([{ cause: 'blur', validate: onBlur }] as const)
353
-
354
- // Use the validationCount for all field instances to
355
- // track freshness of the validation
356
- const validationCount = (this.getInfo().validationCount || 0) + 1
357
- this.getInfo().validationCount = validationCount
358
-
359
- const doValidate = (validate: (typeof validates)[number]['validate']) => {
360
- if (this.options.validator && typeof validate !== 'function') {
361
- return (this.options.validator as Validator<TData>)().validate(
362
- value,
363
- validate,
364
- )
365
- }
366
-
367
- if (this.form.options.validator && typeof validate !== 'function') {
368
- return (this.form.options.validator as Validator<TData>)().validate(
369
- value,
370
- validate,
371
- )
372
- }
373
-
374
- return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
375
- value,
376
- this as never,
377
- )
378
- }
501
+ const validates = getSyncValidatorArray(cause, this.options)
379
502
 
380
503
  // Needs type cast as eslint errantly believes this is always falsy
381
- let hasError = false as boolean
504
+ let hasErrored = false as boolean
382
505
 
383
506
  this.form.store.batch(() => {
384
507
  for (const validateObj of validates) {
385
508
  if (!validateObj.validate) continue
386
- const error = normalizeError(doValidate(validateObj.validate))
509
+ const error = normalizeError(
510
+ this.runValidator({
511
+ validate: validateObj.validate,
512
+ value: { value, fieldApi: this },
513
+ type: 'validate',
514
+ }),
515
+ )
387
516
  const errorMapKey = getErrorMapKey(validateObj.cause)
388
517
  if (this.state.meta.errorMap[errorMapKey] !== error) {
389
518
  this.setMeta((prev) => ({
@@ -393,7 +522,9 @@ export class FieldApi<
393
522
  [getErrorMapKey(validateObj.cause)]: error,
394
523
  },
395
524
  }))
396
- hasError = true
525
+ }
526
+ if (error) {
527
+ hasErrored = true
397
528
  }
398
529
  }
399
530
  })
@@ -406,7 +537,7 @@ export class FieldApi<
406
537
  if (
407
538
  this.state.meta.errorMap[submitErrKey] &&
408
539
  cause !== 'submit' &&
409
- !hasError
540
+ !hasErrored
410
541
  ) {
411
542
  this.setMeta((prev) => ({
412
543
  ...prev,
@@ -417,128 +548,80 @@ export class FieldApi<
417
548
  }))
418
549
  }
419
550
 
420
- // If a sync error is encountered for the errorMapKey (eg. onChange), cancel any async validation
421
- if (hasError) {
422
- this.cancelValidateAsync()
423
- }
424
- }
425
-
426
- __leaseValidateAsync = () => {
427
- const count = (this.getInfo().validationAsyncCount || 0) + 1
428
- this.getInfo().validationAsyncCount = count
429
- return count
430
- }
431
-
432
- cancelValidateAsync = () => {
433
- // Lease a new validation count to ignore any pending validations
434
- this.__leaseValidateAsync()
435
- // Cancel any pending validation state
436
- this.setMeta((prev) => ({
437
- ...prev,
438
- isValidating: false,
439
- }))
551
+ return { hasErrored }
440
552
  }
441
553
 
442
554
  validateAsync = async (value = this.state.value, cause: ValidationCause) => {
443
- const {
444
- onChangeAsync,
445
- onBlurAsync,
446
- onSubmitAsync,
447
- asyncDebounceMs,
448
- onBlurAsyncDebounceMs,
449
- onChangeAsyncDebounceMs,
450
- } = this.options
451
-
452
- const validate =
453
- cause === 'change'
454
- ? onChangeAsync
455
- : cause === 'submit'
456
- ? onSubmitAsync
457
- : onBlurAsync
458
- if (!validate) return []
459
- const debounceMs =
460
- cause === 'submit'
461
- ? 0
462
- : (cause === 'change'
463
- ? onChangeAsyncDebounceMs
464
- : onBlurAsyncDebounceMs) ??
465
- asyncDebounceMs ??
466
- 0
467
-
468
- if (this.state.meta.isValidating !== true) {
555
+ const validates = getAsyncValidatorArray(cause, this.options)
556
+
557
+ if (!this.state.meta.isValidating) {
469
558
  this.setMeta((prev) => ({ ...prev, isValidating: true }))
470
559
  }
471
560
 
472
- // Use the validationCount for all field instances to
473
- // track freshness of the validation
474
- const validationAsyncCount = this.__leaseValidateAsync()
561
+ /**
562
+ * We have to use a for loop and generate our promises this way, otherwise it won't be sync
563
+ * when there are no validators needed to be run
564
+ */
565
+ const promises: Promise<ValidationError | undefined>[] = []
475
566
 
476
- const checkLatest = () =>
477
- validationAsyncCount === this.getInfo().validationAsyncCount
567
+ for (const validateObj of validates) {
568
+ if (!validateObj.validate) continue
569
+ const key = getErrorMapKey(validateObj.cause)
570
+ const fieldValidatorMeta = this.getInfo().validationMetaMap[key]
478
571
 
479
- if (!this.getInfo().validationPromise) {
480
- this.getInfo().validationPromise = new Promise((resolve, reject) => {
481
- this.getInfo().validationResolve = resolve
482
- this.getInfo().validationReject = reject
483
- })
484
- }
572
+ fieldValidatorMeta?.lastAbortController.abort()
573
+ // Sorry Safari 12
574
+ // eslint-disable-next-line compat/compat
575
+ const controller = new AbortController()
485
576
 
486
- if (debounceMs > 0) {
487
- await new Promise((r) => setTimeout(r, debounceMs))
488
- }
489
-
490
- const doValidate = () => {
491
- if (this.options.validator && typeof validate !== 'function') {
492
- return (this.options.validator as Validator<TData>)().validateAsync(
493
- value,
494
- validate,
495
- )
577
+ this.getInfo().validationMetaMap[key] = {
578
+ lastAbortController: controller,
496
579
  }
497
580
 
498
- if (this.form.options.validator && typeof validate !== 'function') {
499
- return (
500
- this.form.options.validator as Validator<TData>
501
- )().validateAsync(value, validate)
502
- }
503
-
504
- return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
505
- value,
506
- this as never,
581
+ promises.push(
582
+ new Promise<ValidationError | undefined>(async (resolve) => {
583
+ let rawError!: ValidationError | undefined
584
+ try {
585
+ rawError = await new Promise((rawResolve, rawReject) => {
586
+ setTimeout(() => {
587
+ if (controller.signal.aborted) return rawResolve(undefined)
588
+ this.runValidator({
589
+ validate: validateObj.validate,
590
+ value: { value, fieldApi: this, signal: controller.signal },
591
+ type: 'validateAsync',
592
+ })
593
+ .then(rawResolve)
594
+ .catch(rawReject)
595
+ }, validateObj.debounceMs)
596
+ })
597
+ } catch (e: unknown) {
598
+ rawError = e as ValidationError
599
+ }
600
+ const error = normalizeError(rawError)
601
+ this.setMeta((prev) => {
602
+ return {
603
+ ...prev,
604
+ errorMap: {
605
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
606
+ ...prev?.errorMap,
607
+ [getErrorMapKey(cause)]: error,
608
+ },
609
+ }
610
+ })
611
+
612
+ resolve(error)
613
+ }),
507
614
  )
508
615
  }
509
616
 
510
- // Only kick off validation if this validation is the latest attempt
511
- if (checkLatest()) {
512
- const prevErrors = this.getMeta().errors
513
- try {
514
- const rawError = await doValidate()
515
- if (checkLatest()) {
516
- const error = normalizeError(rawError)
517
- this.setMeta((prev) => ({
518
- ...prev,
519
- isValidating: false,
520
- errorMap: {
521
- ...prev.errorMap,
522
- [getErrorMapKey(cause)]: error,
523
- },
524
- }))
525
- this.getInfo().validationResolve?.([...prevErrors, error])
526
- }
527
- } catch (error) {
528
- if (checkLatest()) {
529
- this.getInfo().validationReject?.([...prevErrors, error])
530
- throw error
531
- }
532
- } finally {
533
- if (checkLatest()) {
534
- this.setMeta((prev) => ({ ...prev, isValidating: false }))
535
- delete this.getInfo().validationPromise
536
- }
537
- }
617
+ let results: ValidationError[] = []
618
+ if (promises.length) {
619
+ results = await Promise.all(promises)
538
620
  }
539
621
 
540
- // Always return the latest validation promise to the caller
541
- return (await this.getInfo().validationPromise) ?? []
622
+ this.setMeta((prev) => ({ ...prev, isValidating: false }))
623
+
624
+ return results.filter(Boolean)
542
625
  }
543
626
 
544
627
  validate = (
@@ -552,20 +635,11 @@ export class FieldApi<
552
635
  this.form.validate(cause)
553
636
  } catch (_) {}
554
637
 
555
- // Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit)
556
- const errorMapKey = getErrorMapKey(cause)
557
- const prevError = this.getMeta().errorMap[errorMapKey]
558
-
559
638
  // Attempt to sync validate first
560
- this.validateSync(value, cause)
561
-
562
- // If there is a new error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
563
- const newError = this.getMeta().errorMap[errorMapKey]
639
+ const { hasErrored } = this.validateSync(value, cause)
564
640
 
565
- if (prevError !== newError) {
566
- if (!this.options.asyncAlways) {
567
- return this.state.meta.errors
568
- }
641
+ if (hasErrored && !this.options.asyncAlways) {
642
+ return this.state.meta.errors
569
643
  }
570
644
  // No error? Attempt async validation
571
645
  return this.validateAsync(value, cause)