@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/FormApi.ts CHANGED
@@ -1,80 +1,120 @@
1
1
  import { Store } from '@tanstack/store'
2
2
  import type { DeepKeys, DeepValue, Updater } from './utils'
3
3
  import {
4
+ getAsyncValidatorArray,
5
+ getSyncValidatorArray,
4
6
  deleteBy,
5
7
  functionalUpdate,
6
8
  getBy,
7
9
  isNonEmptyArray,
8
10
  setBy,
9
11
  } from './utils'
10
- import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
11
- import type { ValidationError, Validator } from './types'
12
-
13
- type ValidateFn<TData, ValidatorType> = (
14
- values: TData,
15
- formApi: FormApi<TData, ValidatorType>,
16
- ) => ValidationError
17
-
18
- type ValidateOrFn<TData, ValidatorType> = ValidatorType extends Validator<TData>
19
- ? Parameters<ReturnType<ValidatorType>['validate']>[1]
20
- : ValidateFn<TData, ValidatorType>
21
-
22
- type ValidateAsyncFn<TData, ValidatorType> = (
23
- value: TData,
24
- fieldApi: FormApi<TData, ValidatorType>,
25
- ) => ValidationError | Promise<ValidationError>
26
-
27
- export type FormOptions<TData, ValidatorType> = {
28
- defaultValues?: TData
29
- defaultState?: Partial<FormState<TData>>
30
- asyncAlways?: boolean
31
- asyncDebounceMs?: number
32
- validator?: ValidatorType
33
- onMount?: ValidateOrFn<TData, ValidatorType>
34
- onChange?: ValidateOrFn<TData, ValidatorType>
35
- onChangeAsync?: ValidateAsyncFn<TData, ValidatorType>
12
+ import type { FieldApi, FieldMeta } from './FieldApi'
13
+ import type {
14
+ ValidationError,
15
+ ValidationErrorMap,
16
+ Validator,
17
+ ValidationCause,
18
+ ValidationErrorMapKeys,
19
+ } from './types'
20
+
21
+ export type FormValidateFn<
22
+ TFormData,
23
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
24
+ > = (props: {
25
+ value: TFormData
26
+ formApi: FormApi<TFormData, TFormValidator>
27
+ }) => ValidationError
28
+
29
+ export type FormValidateOrFn<
30
+ TFormData,
31
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
32
+ > = TFormValidator extends Validator<TFormData, infer TFN>
33
+ ? TFN
34
+ : FormValidateFn<TFormData, TFormValidator>
35
+
36
+ export type FormValidateAsyncFn<
37
+ TFormData,
38
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
39
+ > = (props: {
40
+ value: TFormData
41
+ formApi: FormApi<TFormData, TFormValidator>
42
+ signal: AbortSignal
43
+ }) => ValidationError | Promise<ValidationError>
44
+
45
+ export type FormAsyncValidateOrFn<
46
+ TFormData,
47
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
48
+ > = TFormValidator extends Validator<TFormData, infer FFN>
49
+ ? FFN | FormValidateAsyncFn<TFormData, TFormValidator>
50
+ : FormValidateAsyncFn<TFormData, TFormValidator>
51
+
52
+ export interface FormValidators<
53
+ TFormData,
54
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
55
+ > {
56
+ onMount?: FormValidateOrFn<TFormData, TFormValidator>
57
+ onChange?: FormValidateOrFn<TFormData, TFormValidator>
58
+ onChangeAsync?: FormAsyncValidateOrFn<TFormData, TFormValidator>
36
59
  onChangeAsyncDebounceMs?: number
37
- onBlur?: ValidateOrFn<TData, ValidatorType>
38
- onBlurAsync?: ValidateAsyncFn<TData, ValidatorType>
60
+ onBlur?: FormValidateOrFn<TFormData, TFormValidator>
61
+ onBlurAsync?: FormAsyncValidateOrFn<TFormData, TFormValidator>
39
62
  onBlurAsyncDebounceMs?: number
40
- onSubmit?: (
41
- values: TData,
42
- formApi: FormApi<TData, ValidatorType>,
43
- ) => any | Promise<any>
44
- onSubmitInvalid?: (
45
- values: TData,
46
- formApi: FormApi<TData, ValidatorType>,
47
- ) => void
63
+ onSubmit?: FormValidateOrFn<TFormData, TFormValidator>
64
+ onSubmitAsync?: FormAsyncValidateOrFn<TFormData, TFormValidator>
65
+ onSubmitAsyncDebounceMs?: number
48
66
  }
49
67
 
50
- export type FieldInfo<TFormData, ValidatorType> = {
51
- instances: Record<string, FieldApi<TFormData, any, unknown, ValidatorType>>
52
- } & ValidationMeta
68
+ export type FormOptions<
69
+ TFormData,
70
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
71
+ > = {
72
+ defaultValues?: TFormData
73
+ defaultState?: Partial<FormState<TFormData>>
74
+ asyncAlways?: boolean
75
+ asyncDebounceMs?: number
76
+ validatorAdapter?: TFormValidator
77
+ validators?: FormValidators<TFormData, TFormValidator>
78
+ onSubmit?: (props: {
79
+ value: TFormData
80
+ formApi: FormApi<TFormData, TFormValidator>
81
+ }) => any | Promise<any>
82
+ onSubmitInvalid?: (props: {
83
+ value: TFormData
84
+ formApi: FormApi<TFormData, TFormValidator>
85
+ }) => void
86
+ }
53
87
 
54
88
  export type ValidationMeta = {
55
- validationCount?: number
56
- validationAsyncCount?: number
57
- validationPromise?: Promise<ValidationError[] | undefined>
58
- validationResolve?: (errors: ValidationError[] | undefined) => void
59
- validationReject?: (errors: unknown) => void
89
+ lastAbortController: AbortController
60
90
  }
61
91
 
62
- export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
63
-
64
- export type ValidationErrorMap = {
65
- [K in ValidationErrorMapKeys]?: ValidationError
92
+ export type FieldInfo<
93
+ TFormData,
94
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
95
+ > = {
96
+ instances: Record<
97
+ string,
98
+ FieldApi<
99
+ TFormData,
100
+ any,
101
+ Validator<unknown, unknown> | undefined,
102
+ TFormValidator
103
+ >
104
+ >
105
+ validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
66
106
  }
67
107
 
68
- export type FormState<TData> = {
69
- values: TData
108
+ export type FormState<TFormData> = {
109
+ values: TFormData
70
110
  // Form Validation
71
111
  isFormValidating: boolean
72
- formValidationCount: number
73
112
  isFormValid: boolean
74
113
  errors: ValidationError[]
75
114
  errorMap: ValidationErrorMap
115
+ validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
76
116
  // Fields
77
- fieldMeta: Record<DeepKeys<TData>, FieldMeta>
117
+ fieldMeta: Record<DeepKeys<TFormData>, FieldMeta>
78
118
  isFieldsValidating: boolean
79
119
  isFieldsValid: boolean
80
120
  isSubmitting: boolean
@@ -87,9 +127,9 @@ export type FormState<TData> = {
87
127
  submissionAttempts: number
88
128
  }
89
129
 
90
- function getDefaultFormState<TData>(
91
- defaultState: Partial<FormState<TData>>,
92
- ): FormState<TData> {
130
+ function getDefaultFormState<TFormData>(
131
+ defaultState: Partial<FormState<TFormData>>,
132
+ ): FormState<TFormData> {
93
133
  return {
94
134
  values: defaultState.values ?? ({} as never),
95
135
  errors: defaultState.errors ?? [],
@@ -106,23 +146,29 @@ function getDefaultFormState<TData>(
106
146
  isValid: defaultState.isValid ?? false,
107
147
  isValidating: defaultState.isValidating ?? false,
108
148
  submissionAttempts: defaultState.submissionAttempts ?? 0,
109
- formValidationCount: defaultState.formValidationCount ?? 0,
149
+ validationMetaMap: defaultState.validationMetaMap ?? {
150
+ onChange: undefined,
151
+ onBlur: undefined,
152
+ onSubmit: undefined,
153
+ onMount: undefined,
154
+ },
110
155
  }
111
156
  }
112
157
 
113
- export class FormApi<TFormData, ValidatorType> {
114
- // // This carries the context for nested fields
115
- options: FormOptions<TFormData, ValidatorType> = {}
158
+ export class FormApi<
159
+ TFormData,
160
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
161
+ > {
162
+ options: FormOptions<TFormData, TFormValidator> = {}
116
163
  store!: Store<FormState<TFormData>>
117
164
  // Do not use __state directly, as it is not reactive.
118
165
  // Please use form.useStore() utility to subscribe to state
119
166
  state!: FormState<TFormData>
120
- fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>> =
167
+ // // This carries the context for nested fields
168
+ fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, TFormValidator>> =
121
169
  {} as any
122
- fieldName?: string
123
- validationMeta: ValidationMeta = {}
124
170
 
125
- constructor(opts?: FormOptions<TFormData, ValidatorType>) {
171
+ constructor(opts?: FormOptions<TFormData, TFormValidator>) {
126
172
  this.store = new Store<FormState<TFormData>>(
127
173
  getDefaultFormState({
128
174
  ...(opts?.defaultState as any),
@@ -181,24 +227,35 @@ export class FormApi<TFormData, ValidatorType> {
181
227
  this.update(opts || {})
182
228
  }
183
229
 
184
- mount = () => {
185
- const doValidate = () => {
186
- if (
187
- this.options.validator &&
188
- typeof this.options.onMount !== 'function'
189
- ) {
190
- return (this.options.validator as Validator<TFormData>)().validate(
191
- this.state.values,
192
- this.options.onMount,
193
- )
194
- }
195
- return (this.options.onMount as ValidateFn<TFormData, ValidatorType>)(
196
- this.state.values,
197
- this,
198
- )
230
+ runValidator<
231
+ TValue extends { value: TFormData; formApi: FormApi<any, any> },
232
+ TType extends 'validate' | 'validateAsync',
233
+ >(props: {
234
+ validate: TType extends 'validate'
235
+ ? FormValidateOrFn<TFormData, TFormValidator>
236
+ : FormAsyncValidateOrFn<TFormData, TFormValidator>
237
+ value: TValue
238
+ type: TType
239
+ }): ReturnType<ReturnType<Validator<any>>[TType]> {
240
+ const adapter = this.options.validatorAdapter
241
+ if (adapter && typeof props.validate !== 'function') {
242
+ return adapter()[props.type](props.value, props.validate) as never
199
243
  }
200
- if (!this.options.onMount) return
201
- const error = doValidate()
244
+
245
+ return (props.validate as FormValidateFn<any, any>)(props.value) as never
246
+ }
247
+
248
+ mount = () => {
249
+ const { onMount } = this.options.validators || {}
250
+ if (!onMount) return
251
+ const error = this.runValidator({
252
+ validate: onMount,
253
+ value: {
254
+ value: this.state.values,
255
+ formApi: this,
256
+ },
257
+ type: 'validate',
258
+ })
202
259
  if (error) {
203
260
  this.store.setState((prev) => ({
204
261
  ...prev,
@@ -207,7 +264,7 @@ export class FormApi<TFormData, ValidatorType> {
207
264
  }
208
265
  }
209
266
 
210
- update = (options?: FormOptions<TFormData, ValidatorType>) => {
267
+ update = (options?: FormOptions<TFormData, TFormValidator>) => {
211
268
  if (!options) return
212
269
 
213
270
  this.store.batch(() => {
@@ -253,190 +310,171 @@ export class FormApi<TFormData, ValidatorType> {
253
310
  const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
254
311
  this.store.batch(() => {
255
312
  void (
256
- Object.values(this.fieldInfo) as FieldInfo<any, ValidatorType>[]
313
+ Object.values(this.fieldInfo) as FieldInfo<any, TFormValidator>[]
257
314
  ).forEach((field) => {
258
315
  Object.values(field.instances).forEach((instance) => {
316
+ // Validate the field
317
+ fieldValidationPromises.push(
318
+ Promise.resolve().then(() => instance.validate(cause)),
319
+ )
259
320
  // If any fields are not touched
260
321
  if (!instance.state.meta.isTouched) {
261
322
  // Mark them as touched
262
323
  instance.setMeta((prev) => ({ ...prev, isTouched: true }))
263
- // Validate the field
264
- fieldValidationPromises.push(
265
- Promise.resolve().then(() => instance.validate(cause)),
266
- )
267
324
  }
268
325
  })
269
326
  })
270
327
  })
271
328
 
272
- return Promise.all(fieldValidationPromises)
329
+ const fieldErrorMapMap = await Promise.all(fieldValidationPromises)
330
+ return fieldErrorMapMap.flat()
273
331
  }
274
332
 
275
- validateSync = (cause: ValidationCause): void => {
276
- const { onChange, onBlur } = this.options
277
- const validate =
278
- cause === 'change' ? onChange : cause === 'blur' ? onBlur : undefined
279
- if (!validate) return
280
-
281
- const errorMapKey = getErrorMapKey(cause)
282
- const doValidate = () => {
283
- if (this.options.validator && typeof validate !== 'function') {
284
- return (this.options.validator as Validator<TFormData>)().validate(
285
- this.state.values,
286
- validate,
333
+ // TODO: This code is copied from FieldApi, we should refactor to share
334
+ validateSync = (cause: ValidationCause) => {
335
+ const validates = getSyncValidatorArray(cause, this.options)
336
+ let hasErrored = false as boolean
337
+
338
+ this.store.batch(() => {
339
+ for (const validateObj of validates) {
340
+ if (!validateObj.validate) continue
341
+
342
+ const error = normalizeError(
343
+ this.runValidator({
344
+ validate: validateObj.validate,
345
+ value: {
346
+ value: this.state.values,
347
+ formApi: this,
348
+ },
349
+ type: 'validate',
350
+ }),
287
351
  )
352
+ const errorMapKey = getErrorMapKey(validateObj.cause)
353
+ if (this.state.errorMap[errorMapKey] !== error) {
354
+ this.store.setState((prev) => ({
355
+ ...prev,
356
+ errorMap: {
357
+ ...prev.errorMap,
358
+ [errorMapKey]: error,
359
+ },
360
+ }))
361
+ }
362
+ if (error) {
363
+ hasErrored = true
364
+ }
288
365
  }
366
+ })
289
367
 
290
- return (validate as ValidateFn<TFormData, ValidatorType>)(
291
- this.state.values,
292
- this,
293
- )
294
- }
295
-
296
- const error = normalizeError(doValidate())
297
- if (this.state.errorMap[errorMapKey] !== error) {
368
+ /**
369
+ * when we have an error for onSubmit in the state, we want
370
+ * to clear the error as soon as the user enters a valid value in the field
371
+ */
372
+ const submitErrKey = getErrorMapKey('submit')
373
+ if (
374
+ this.state.errorMap[submitErrKey] &&
375
+ cause !== 'submit' &&
376
+ !hasErrored
377
+ ) {
298
378
  this.store.setState((prev) => ({
299
379
  ...prev,
300
380
  errorMap: {
301
381
  ...prev.errorMap,
302
- [errorMapKey]: error,
382
+ [submitErrKey]: undefined,
303
383
  },
304
384
  }))
305
385
  }
306
386
 
307
- if (this.state.errorMap[errorMapKey]) {
308
- this.cancelValidateAsync()
309
- }
310
- }
311
-
312
- __leaseValidateAsync = () => {
313
- const count = (this.validationMeta.validationAsyncCount || 0) + 1
314
- this.validationMeta.validationAsyncCount = count
315
- return count
316
- }
317
-
318
- cancelValidateAsync = () => {
319
- // Lease a new validation count to ignore any pending validations
320
- this.__leaseValidateAsync()
321
- // Cancel any pending validation state
322
- this.store.setState((prev) => ({
323
- ...prev,
324
- isFormValidating: false,
325
- }))
387
+ return { hasErrored }
326
388
  }
327
389
 
328
390
  validateAsync = async (
329
391
  cause: ValidationCause,
330
392
  ): Promise<ValidationError[]> => {
331
- const {
332
- onChangeAsync,
333
- onBlurAsync,
334
- asyncDebounceMs,
335
- onBlurAsyncDebounceMs,
336
- onChangeAsyncDebounceMs,
337
- } = this.options
338
-
339
- const validate =
340
- cause === 'change'
341
- ? onChangeAsync
342
- : cause === 'blur'
343
- ? onBlurAsync
344
- : undefined
345
-
346
- if (!validate) return []
347
- const debounceMs =
348
- (cause === 'change' ? onChangeAsyncDebounceMs : onBlurAsyncDebounceMs) ??
349
- asyncDebounceMs ??
350
- 0
393
+ const validates = getAsyncValidatorArray(cause, this.options)
351
394
 
352
395
  if (!this.state.isFormValidating) {
353
396
  this.store.setState((prev) => ({ ...prev, isFormValidating: true }))
354
397
  }
355
398
 
356
- // Use the validationCount for all field instances to
357
- // track freshness of the validation
358
- const validationAsyncCount = this.__leaseValidateAsync()
399
+ /**
400
+ * We have to use a for loop and generate our promises this way, otherwise it won't be sync
401
+ * when there are no validators needed to be run
402
+ */
403
+ const promises: Promise<ValidationError | undefined>[] = []
359
404
 
360
- const checkLatest = () =>
361
- validationAsyncCount === this.validationMeta.validationAsyncCount
405
+ for (const validateObj of validates) {
406
+ if (!validateObj.validate) continue
407
+ const key = getErrorMapKey(validateObj.cause)
408
+ const fieldValidatorMeta = this.state.validationMetaMap[key]
362
409
 
363
- if (!this.validationMeta.validationPromise) {
364
- this.validationMeta.validationPromise = new Promise((resolve, reject) => {
365
- this.validationMeta.validationResolve = resolve
366
- this.validationMeta.validationReject = reject
367
- })
368
- }
410
+ fieldValidatorMeta?.lastAbortController.abort()
411
+ // Sorry Safari 12
412
+ // eslint-disable-next-line compat/compat
413
+ const controller = new AbortController()
369
414
 
370
- if (debounceMs > 0) {
371
- await new Promise((r) => setTimeout(r, debounceMs))
372
- }
373
-
374
- const doValidate = () => {
375
- if (typeof validate === 'function') {
376
- return validate(this.state.values, this) as ValidationError
377
- }
378
- if (this.options.validator && typeof validate !== 'function') {
379
- return (this.options.validator as Validator<TFormData>)().validateAsync(
380
- this.state.values,
381
- validate,
382
- )
415
+ this.state.validationMetaMap[key] = {
416
+ lastAbortController: controller,
383
417
  }
384
- const errorMapKey = getErrorMapKey(cause)
385
- throw new Error(
386
- `Form validation for ${errorMapKey}Async failed. ${errorMapKey}Async should either be a function, or \`validator\` should be correct.`,
387
- )
388
- }
389
418
 
390
- // Only kick off validation if this validation is the latest attempt
391
- if (checkLatest()) {
392
- const prevErrors = this.state.errors
393
- try {
394
- const rawError = await doValidate()
395
- if (checkLatest()) {
419
+ promises.push(
420
+ new Promise<ValidationError | undefined>(async (resolve) => {
421
+ let rawError!: ValidationError | undefined
422
+ try {
423
+ rawError = await new Promise((rawResolve, rawReject) => {
424
+ setTimeout(() => {
425
+ if (controller.signal.aborted) return rawResolve(undefined)
426
+ this.runValidator({
427
+ validate: validateObj.validate!,
428
+ value: {
429
+ value: this.state.values,
430
+ formApi: this,
431
+ signal: controller.signal,
432
+ },
433
+ type: 'validateAsync',
434
+ })
435
+ .then(rawResolve)
436
+ .catch(rawReject)
437
+ }, validateObj.debounceMs)
438
+ })
439
+ } catch (e: unknown) {
440
+ rawError = e as ValidationError
441
+ }
396
442
  const error = normalizeError(rawError)
397
443
  this.store.setState((prev) => ({
398
444
  ...prev,
399
- isFormValidating: false,
400
445
  errorMap: {
401
446
  ...prev.errorMap,
402
447
  [getErrorMapKey(cause)]: error,
403
448
  },
404
449
  }))
405
- this.validationMeta.validationResolve?.([...prevErrors, error])
406
- }
407
- } catch (error) {
408
- if (checkLatest()) {
409
- this.validationMeta.validationReject?.([...prevErrors, error])
410
- throw error
411
- }
412
- } finally {
413
- if (checkLatest()) {
414
- this.store.setState((prev) => ({ ...prev, isFormValidating: false }))
415
- delete this.validationMeta.validationPromise
416
- }
417
- }
450
+
451
+ resolve(error)
452
+ }),
453
+ )
454
+ }
455
+
456
+ let results: ValidationError[] = []
457
+ if (promises.length) {
458
+ results = await Promise.all(promises)
418
459
  }
419
- // Always return the latest validation promise to the caller
420
- return (await this.validationMeta.validationPromise) ?? []
460
+
461
+ this.store.setState((prev) => ({
462
+ ...prev,
463
+ isFormValidating: false,
464
+ }))
465
+
466
+ return results.filter(Boolean)
421
467
  }
422
468
 
423
469
  validate = (
424
470
  cause: ValidationCause,
425
471
  ): ValidationError[] | Promise<ValidationError[]> => {
426
- // Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit)
427
- const errorMapKey = getErrorMapKey(cause)
428
- const prevError = this.state.errorMap[errorMapKey]
429
-
430
472
  // Attempt to sync validate first
431
- this.validateSync(cause)
473
+ const { hasErrored } = this.validateSync(cause)
432
474
 
433
- const newError = this.state.errorMap[errorMapKey]
434
- if (
435
- prevError !== newError &&
436
- !this.options.asyncAlways &&
437
- !(newError === undefined && prevError !== undefined)
438
- )
475
+ if (hasErrored && !this.options.asyncAlways) {
439
476
  return this.state.errors
477
+ }
440
478
 
441
479
  // No error? Attempt async validation
442
480
  return this.validateAsync(cause)
@@ -471,7 +509,10 @@ export class FormApi<TFormData, ValidatorType> {
471
509
  // Fields are invalid, do not submit
472
510
  if (!this.state.isFieldsValid) {
473
511
  done()
474
- this.options.onSubmitInvalid?.(this.state.values, this)
512
+ this.options.onSubmitInvalid?.({
513
+ value: this.state.values,
514
+ formApi: this,
515
+ })
475
516
  return
476
517
  }
477
518
 
@@ -480,13 +521,16 @@ export class FormApi<TFormData, ValidatorType> {
480
521
 
481
522
  if (!this.state.isValid) {
482
523
  done()
483
- this.options.onSubmitInvalid?.(this.state.values, this)
524
+ this.options.onSubmitInvalid?.({
525
+ value: this.state.values,
526
+ formApi: this,
527
+ })
484
528
  return
485
529
  }
486
530
 
487
531
  try {
488
532
  // Run the submit code
489
- await this.options.onSubmit?.(this.state.values, this)
533
+ await this.options.onSubmit?.({ value: this.state.values, formApi: this })
490
534
 
491
535
  this.store.batch(() => {
492
536
  this.store.setState((prev) => ({ ...prev, isSubmitted: true }))
@@ -510,10 +554,16 @@ export class FormApi<TFormData, ValidatorType> {
510
554
 
511
555
  getFieldInfo = <TField extends DeepKeys<TFormData>>(
512
556
  field: TField,
513
- ): FieldInfo<TFormData, ValidatorType> => {
557
+ ): FieldInfo<TFormData, TFormValidator> => {
514
558
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
515
559
  return (this.fieldInfo[field] ||= {
516
560
  instances: {},
561
+ validationMetaMap: {
562
+ onChange: undefined,
563
+ onBlur: undefined,
564
+ onSubmit: undefined,
565
+ onMount: undefined,
566
+ },
517
567
  })
518
568
  }
519
569