@tanstack/form-core 0.10.3 → 0.12.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 (104) hide show
  1. package/dist/cjs/FieldApi.d.ts +95 -0
  2. package/dist/cjs/FormApi.d.ts +118 -0
  3. package/dist/cjs/index.cjs +926 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/cjs/index.d.cts +5 -0
  6. package/dist/cjs/index.d.ts +5 -0
  7. package/dist/cjs/index.js +926 -0
  8. package/dist/cjs/mergeForm.d.ts +4 -0
  9. package/dist/cjs/tests/FieldApi.spec.d.ts +1 -0
  10. package/dist/cjs/tests/FieldApi.test-d.d.ts +1 -0
  11. package/dist/cjs/tests/FormApi.spec.d.ts +1 -0
  12. package/dist/cjs/tests/mutateMergeDeep.spec.d.ts +1 -0
  13. package/dist/cjs/tests/utils.d.ts +1 -0
  14. package/dist/cjs/tests/utils.spec.d.ts +1 -0
  15. package/dist/cjs/types.d.ts +14 -0
  16. package/dist/cjs/utils.d.ts +57 -0
  17. package/dist/mjs/FieldApi.d.ts +95 -0
  18. package/dist/mjs/FormApi.d.ts +118 -0
  19. package/dist/mjs/index.d.mts +5 -0
  20. package/dist/mjs/index.d.ts +5 -0
  21. package/dist/mjs/index.js +926 -0
  22. package/dist/mjs/index.mjs +926 -0
  23. package/dist/mjs/index.mjs.map +1 -0
  24. package/dist/mjs/mergeForm.d.ts +4 -0
  25. package/dist/mjs/tests/FieldApi.spec.d.ts +1 -0
  26. package/dist/mjs/tests/FieldApi.test-d.d.ts +1 -0
  27. package/dist/mjs/tests/FormApi.spec.d.ts +1 -0
  28. package/dist/mjs/tests/mutateMergeDeep.spec.d.ts +1 -0
  29. package/dist/mjs/tests/utils.d.ts +1 -0
  30. package/dist/mjs/tests/utils.spec.d.ts +1 -0
  31. package/dist/mjs/types.d.ts +14 -0
  32. package/dist/mjs/utils.d.ts +57 -0
  33. package/package.json +16 -21
  34. package/src/FieldApi.ts +328 -236
  35. package/src/FormApi.ts +302 -216
  36. package/src/index.ts +1 -0
  37. package/src/mergeForm.ts +42 -0
  38. package/src/tests/FieldApi.spec.ts +135 -48
  39. package/src/tests/FieldApi.test-d.ts +10 -6
  40. package/src/tests/FormApi.spec.ts +171 -62
  41. package/src/tests/mutateMergeDeep.spec.ts +32 -0
  42. package/src/tests/utils.ts +1 -1
  43. package/src/types.ts +11 -2
  44. package/src/utils.ts +137 -14
  45. package/build/legacy/FieldApi.cjs +0 -340
  46. package/build/legacy/FieldApi.cjs.map +0 -1
  47. package/build/legacy/FieldApi.d.cts +0 -4
  48. package/build/legacy/FieldApi.d.ts +0 -4
  49. package/build/legacy/FieldApi.js +0 -315
  50. package/build/legacy/FieldApi.js.map +0 -1
  51. package/build/legacy/FormApi.cjs +0 -438
  52. package/build/legacy/FormApi.cjs.map +0 -1
  53. package/build/legacy/FormApi.d.cts +0 -4
  54. package/build/legacy/FormApi.d.ts +0 -4
  55. package/build/legacy/FormApi.js +0 -419
  56. package/build/legacy/FormApi.js.map +0 -1
  57. package/build/legacy/index.cjs +0 -31
  58. package/build/legacy/index.cjs.map +0 -1
  59. package/build/legacy/index.d.cts +0 -170
  60. package/build/legacy/index.d.ts +0 -170
  61. package/build/legacy/index.js +0 -6
  62. package/build/legacy/index.js.map +0 -1
  63. package/build/legacy/types.cjs +0 -19
  64. package/build/legacy/types.cjs.map +0 -1
  65. package/build/legacy/types.d.cts +0 -7
  66. package/build/legacy/types.d.ts +0 -7
  67. package/build/legacy/types.js +0 -1
  68. package/build/legacy/types.js.map +0 -1
  69. package/build/legacy/utils.cjs +0 -132
  70. package/build/legacy/utils.cjs.map +0 -1
  71. package/build/legacy/utils.d.cts +0 -37
  72. package/build/legacy/utils.d.ts +0 -37
  73. package/build/legacy/utils.js +0 -103
  74. package/build/legacy/utils.js.map +0 -1
  75. package/build/modern/FieldApi.cjs +0 -337
  76. package/build/modern/FieldApi.cjs.map +0 -1
  77. package/build/modern/FieldApi.d.cts +0 -4
  78. package/build/modern/FieldApi.d.ts +0 -4
  79. package/build/modern/FieldApi.js +0 -312
  80. package/build/modern/FieldApi.js.map +0 -1
  81. package/build/modern/FormApi.cjs +0 -431
  82. package/build/modern/FormApi.cjs.map +0 -1
  83. package/build/modern/FormApi.d.cts +0 -4
  84. package/build/modern/FormApi.d.ts +0 -4
  85. package/build/modern/FormApi.js +0 -412
  86. package/build/modern/FormApi.js.map +0 -1
  87. package/build/modern/index.cjs +0 -31
  88. package/build/modern/index.cjs.map +0 -1
  89. package/build/modern/index.d.cts +0 -170
  90. package/build/modern/index.d.ts +0 -170
  91. package/build/modern/index.js +0 -6
  92. package/build/modern/index.js.map +0 -1
  93. package/build/modern/types.cjs +0 -19
  94. package/build/modern/types.cjs.map +0 -1
  95. package/build/modern/types.d.cts +0 -7
  96. package/build/modern/types.d.ts +0 -7
  97. package/build/modern/types.js +0 -1
  98. package/build/modern/types.js.map +0 -1
  99. package/build/modern/utils.cjs +0 -132
  100. package/build/modern/utils.cjs.map +0 -1
  101. package/build/modern/utils.d.cts +0 -37
  102. package/build/modern/utils.d.ts +0 -37
  103. package/build/modern/utils.js +0 -103
  104. package/build/modern/utils.js.map +0 -1
package/src/FormApi.ts CHANGED
@@ -1,80 +1,131 @@
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 interface FormTransform<
69
+ TFormData,
70
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
71
+ > {
72
+ fn: (
73
+ formBase: FormApi<TFormData, TFormValidator>,
74
+ ) => FormApi<TFormData, TFormValidator>
75
+ deps: unknown[]
76
+ }
53
77
 
54
- 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
78
+ export interface FormOptions<
79
+ TFormData,
80
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
81
+ > {
82
+ defaultValues?: TFormData
83
+ defaultState?: Partial<FormState<TFormData>>
84
+ asyncAlways?: boolean
85
+ asyncDebounceMs?: number
86
+ validatorAdapter?: TFormValidator
87
+ validators?: FormValidators<TFormData, TFormValidator>
88
+ onSubmit?: (props: {
89
+ value: TFormData
90
+ formApi: FormApi<TFormData, TFormValidator>
91
+ }) => any | Promise<any>
92
+ onSubmitInvalid?: (props: {
93
+ value: TFormData
94
+ formApi: FormApi<TFormData, TFormValidator>
95
+ }) => void
96
+ transform?: FormTransform<TFormData, TFormValidator>
60
97
  }
61
98
 
62
- export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
99
+ export type ValidationMeta = {
100
+ lastAbortController: AbortController
101
+ }
63
102
 
64
- export type ValidationErrorMap = {
65
- [K in ValidationErrorMapKeys]?: ValidationError
103
+ export type FieldInfo<
104
+ TFormData,
105
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
106
+ > = {
107
+ instances: Record<
108
+ string,
109
+ FieldApi<
110
+ TFormData,
111
+ any,
112
+ Validator<unknown, unknown> | undefined,
113
+ TFormValidator
114
+ >
115
+ >
116
+ validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
66
117
  }
67
118
 
68
- export type FormState<TData> = {
69
- values: TData
119
+ export type FormState<TFormData> = {
120
+ values: TFormData
70
121
  // Form Validation
71
122
  isFormValidating: boolean
72
- formValidationCount: number
73
123
  isFormValid: boolean
74
124
  errors: ValidationError[]
75
125
  errorMap: ValidationErrorMap
126
+ validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
76
127
  // Fields
77
- fieldMeta: Record<DeepKeys<TData>, FieldMeta>
128
+ fieldMeta: Record<DeepKeys<TFormData>, FieldMeta>
78
129
  isFieldsValidating: boolean
79
130
  isFieldsValid: boolean
80
131
  isSubmitting: boolean
@@ -87,9 +138,9 @@ export type FormState<TData> = {
87
138
  submissionAttempts: number
88
139
  }
89
140
 
90
- function getDefaultFormState<TData>(
91
- defaultState: Partial<FormState<TData>>,
92
- ): FormState<TData> {
141
+ function getDefaultFormState<TFormData>(
142
+ defaultState: Partial<FormState<TFormData>>,
143
+ ): FormState<TFormData> {
93
144
  return {
94
145
  values: defaultState.values ?? ({} as never),
95
146
  errors: defaultState.errors ?? [],
@@ -106,23 +157,32 @@ function getDefaultFormState<TData>(
106
157
  isValid: defaultState.isValid ?? false,
107
158
  isValidating: defaultState.isValidating ?? false,
108
159
  submissionAttempts: defaultState.submissionAttempts ?? 0,
109
- formValidationCount: defaultState.formValidationCount ?? 0,
160
+ validationMetaMap: defaultState.validationMetaMap ?? {
161
+ onChange: undefined,
162
+ onBlur: undefined,
163
+ onSubmit: undefined,
164
+ onMount: undefined,
165
+ onServer: undefined,
166
+ },
110
167
  }
111
168
  }
112
169
 
113
- export class FormApi<TFormData, ValidatorType> {
114
- // // This carries the context for nested fields
115
- options: FormOptions<TFormData, ValidatorType> = {}
170
+ export class FormApi<
171
+ TFormData,
172
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
173
+ > {
174
+ options: FormOptions<TFormData, TFormValidator> = {}
116
175
  store!: Store<FormState<TFormData>>
117
176
  // Do not use __state directly, as it is not reactive.
118
177
  // Please use form.useStore() utility to subscribe to state
119
178
  state!: FormState<TFormData>
120
- fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>> =
179
+ // // This carries the context for nested fields
180
+ fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, TFormValidator>> =
121
181
  {} as any
122
- fieldName?: string
123
- validationMeta: ValidationMeta = {}
124
182
 
125
- constructor(opts?: FormOptions<TFormData, ValidatorType>) {
183
+ prevTransformArray: unknown[] = []
184
+
185
+ constructor(opts?: FormOptions<TFormData, TFormValidator>) {
126
186
  this.store = new Store<FormState<TFormData>>(
127
187
  getDefaultFormState({
128
188
  ...(opts?.defaultState as any),
@@ -170,8 +230,21 @@ export class FormApi<TFormData, ValidatorType> {
170
230
  isTouched,
171
231
  }
172
232
 
173
- this.store.state = state
174
233
  this.state = state
234
+ this.store.state = this.state
235
+
236
+ // Only run transform if state has shallowly changed - IE how React.useEffect works
237
+ const transformArray = this.options.transform?.deps ?? []
238
+ const shouldTransform =
239
+ transformArray.length !== this.prevTransformArray.length ||
240
+ transformArray.some((val, i) => val !== this.prevTransformArray[i])
241
+
242
+ if (shouldTransform) {
243
+ // This mutates the state
244
+ this.options.transform?.fn(this)
245
+ this.store.state = this.state
246
+ this.prevTransformArray = transformArray
247
+ }
175
248
  },
176
249
  },
177
250
  )
@@ -181,24 +254,35 @@ export class FormApi<TFormData, ValidatorType> {
181
254
  this.update(opts || {})
182
255
  }
183
256
 
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
- )
257
+ runValidator<
258
+ TValue extends { value: TFormData; formApi: FormApi<any, any> },
259
+ TType extends 'validate' | 'validateAsync',
260
+ >(props: {
261
+ validate: TType extends 'validate'
262
+ ? FormValidateOrFn<TFormData, TFormValidator>
263
+ : FormAsyncValidateOrFn<TFormData, TFormValidator>
264
+ value: TValue
265
+ type: TType
266
+ }): ReturnType<ReturnType<Validator<any>>[TType]> {
267
+ const adapter = this.options.validatorAdapter
268
+ if (adapter && typeof props.validate !== 'function') {
269
+ return adapter()[props.type](props.value, props.validate) as never
199
270
  }
200
- if (!this.options.onMount) return
201
- const error = doValidate()
271
+
272
+ return (props.validate as FormValidateFn<any, any>)(props.value) as never
273
+ }
274
+
275
+ mount = () => {
276
+ const { onMount } = this.options.validators || {}
277
+ if (!onMount) return
278
+ const error = this.runValidator({
279
+ validate: onMount,
280
+ value: {
281
+ value: this.state.values,
282
+ formApi: this,
283
+ },
284
+ type: 'validate',
285
+ })
202
286
  if (error) {
203
287
  this.store.setState((prev) => ({
204
288
  ...prev,
@@ -207,17 +291,22 @@ export class FormApi<TFormData, ValidatorType> {
207
291
  }
208
292
  }
209
293
 
210
- update = (options?: FormOptions<TFormData, ValidatorType>) => {
294
+ update = (options?: FormOptions<TFormData, TFormValidator>) => {
211
295
  if (!options) return
212
296
 
297
+ const oldOptions = this.options
298
+
299
+ // Options need to be updated first so that when the store is updated, the state is correct for the derived state
300
+ this.options = options
301
+
213
302
  this.store.batch(() => {
214
303
  const shouldUpdateValues =
215
304
  options.defaultValues &&
216
- options.defaultValues !== this.options.defaultValues &&
305
+ options.defaultValues !== oldOptions.defaultValues &&
217
306
  !this.state.isTouched
218
307
 
219
308
  const shouldUpdateState =
220
- options.defaultState !== this.options.defaultState &&
309
+ options.defaultState !== oldOptions.defaultState &&
221
310
  !this.state.isTouched
222
311
 
223
312
  this.store.setState(() =>
@@ -237,8 +326,6 @@ export class FormApi<TFormData, ValidatorType> {
237
326
  ),
238
327
  )
239
328
  })
240
-
241
- this.options = options
242
329
  }
243
330
 
244
331
  reset = () =>
@@ -253,7 +340,7 @@ export class FormApi<TFormData, ValidatorType> {
253
340
  const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
254
341
  this.store.batch(() => {
255
342
  void (
256
- Object.values(this.fieldInfo) as FieldInfo<any, ValidatorType>[]
343
+ Object.values(this.fieldInfo) as FieldInfo<any, TFormValidator>[]
257
344
  ).forEach((field) => {
258
345
  Object.values(field.instances).forEach((instance) => {
259
346
  // Validate the field
@@ -269,174 +356,157 @@ export class FormApi<TFormData, ValidatorType> {
269
356
  })
270
357
  })
271
358
 
272
- return Promise.all(fieldValidationPromises)
359
+ const fieldErrorMapMap = await Promise.all(fieldValidationPromises)
360
+ return fieldErrorMapMap.flat()
273
361
  }
274
362
 
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,
363
+ // TODO: This code is copied from FieldApi, we should refactor to share
364
+ validateSync = (cause: ValidationCause) => {
365
+ const validates = getSyncValidatorArray(cause, this.options)
366
+ let hasErrored = false as boolean
367
+
368
+ this.store.batch(() => {
369
+ for (const validateObj of validates) {
370
+ if (!validateObj.validate) continue
371
+
372
+ const error = normalizeError(
373
+ this.runValidator({
374
+ validate: validateObj.validate,
375
+ value: {
376
+ value: this.state.values,
377
+ formApi: this,
378
+ },
379
+ type: 'validate',
380
+ }),
287
381
  )
382
+ const errorMapKey = getErrorMapKey(validateObj.cause)
383
+ if (this.state.errorMap[errorMapKey] !== error) {
384
+ this.store.setState((prev) => ({
385
+ ...prev,
386
+ errorMap: {
387
+ ...prev.errorMap,
388
+ [errorMapKey]: error,
389
+ },
390
+ }))
391
+ }
392
+ if (error) {
393
+ hasErrored = true
394
+ }
288
395
  }
396
+ })
289
397
 
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) {
398
+ /**
399
+ * when we have an error for onSubmit in the state, we want
400
+ * to clear the error as soon as the user enters a valid value in the field
401
+ */
402
+ const submitErrKey = getErrorMapKey('submit')
403
+ if (
404
+ this.state.errorMap[submitErrKey] &&
405
+ cause !== 'submit' &&
406
+ !hasErrored
407
+ ) {
298
408
  this.store.setState((prev) => ({
299
409
  ...prev,
300
410
  errorMap: {
301
411
  ...prev.errorMap,
302
- [errorMapKey]: error,
412
+ [submitErrKey]: undefined,
303
413
  },
304
414
  }))
305
415
  }
306
416
 
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
- }))
417
+ return { hasErrored }
326
418
  }
327
419
 
328
420
  validateAsync = async (
329
421
  cause: ValidationCause,
330
422
  ): 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
423
+ const validates = getAsyncValidatorArray(cause, this.options)
351
424
 
352
425
  if (!this.state.isFormValidating) {
353
426
  this.store.setState((prev) => ({ ...prev, isFormValidating: true }))
354
427
  }
355
428
 
356
- // Use the validationCount for all field instances to
357
- // track freshness of the validation
358
- const validationAsyncCount = this.__leaseValidateAsync()
359
-
360
- const checkLatest = () =>
361
- validationAsyncCount === this.validationMeta.validationAsyncCount
429
+ /**
430
+ * We have to use a for loop and generate our promises this way, otherwise it won't be sync
431
+ * when there are no validators needed to be run
432
+ */
433
+ const promises: Promise<ValidationError | undefined>[] = []
362
434
 
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
- }
435
+ for (const validateObj of validates) {
436
+ if (!validateObj.validate) continue
437
+ const key = getErrorMapKey(validateObj.cause)
438
+ const fieldValidatorMeta = this.state.validationMetaMap[key]
369
439
 
370
- if (debounceMs > 0) {
371
- await new Promise((r) => setTimeout(r, debounceMs))
372
- }
440
+ fieldValidatorMeta?.lastAbortController.abort()
441
+ const controller = new AbortController()
373
442
 
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
- )
443
+ this.state.validationMetaMap[key] = {
444
+ lastAbortController: controller,
383
445
  }
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
446
 
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()) {
447
+ promises.push(
448
+ new Promise<ValidationError | undefined>(async (resolve) => {
449
+ let rawError!: ValidationError | undefined
450
+ try {
451
+ rawError = await new Promise((rawResolve, rawReject) => {
452
+ setTimeout(async () => {
453
+ if (controller.signal.aborted) return rawResolve(undefined)
454
+ try {
455
+ rawResolve(
456
+ await this.runValidator({
457
+ validate: validateObj.validate!,
458
+ value: {
459
+ value: this.state.values,
460
+ formApi: this,
461
+ signal: controller.signal,
462
+ },
463
+ type: 'validateAsync',
464
+ }),
465
+ )
466
+ } catch (e) {
467
+ rawReject(e)
468
+ }
469
+ }, validateObj.debounceMs)
470
+ })
471
+ } catch (e: unknown) {
472
+ rawError = e as ValidationError
473
+ }
396
474
  const error = normalizeError(rawError)
397
475
  this.store.setState((prev) => ({
398
476
  ...prev,
399
- isFormValidating: false,
400
477
  errorMap: {
401
478
  ...prev.errorMap,
402
479
  [getErrorMapKey(cause)]: error,
403
480
  },
404
481
  }))
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
- }
482
+
483
+ resolve(error)
484
+ }),
485
+ )
486
+ }
487
+
488
+ let results: ValidationError[] = []
489
+ if (promises.length) {
490
+ results = await Promise.all(promises)
418
491
  }
419
- // Always return the latest validation promise to the caller
420
- return (await this.validationMeta.validationPromise) ?? []
492
+
493
+ this.store.setState((prev) => ({
494
+ ...prev,
495
+ isFormValidating: false,
496
+ }))
497
+
498
+ return results.filter(Boolean)
421
499
  }
422
500
 
423
501
  validate = (
424
502
  cause: ValidationCause,
425
503
  ): 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
504
  // Attempt to sync validate first
431
- this.validateSync(cause)
505
+ const { hasErrored } = this.validateSync(cause)
432
506
 
433
- const newError = this.state.errorMap[errorMapKey]
434
- if (
435
- prevError !== newError &&
436
- !this.options.asyncAlways &&
437
- !(newError === undefined && prevError !== undefined)
438
- )
507
+ if (hasErrored && !this.options.asyncAlways) {
439
508
  return this.state.errors
509
+ }
440
510
 
441
511
  // No error? Attempt async validation
442
512
  return this.validateAsync(cause)
@@ -471,7 +541,10 @@ export class FormApi<TFormData, ValidatorType> {
471
541
  // Fields are invalid, do not submit
472
542
  if (!this.state.isFieldsValid) {
473
543
  done()
474
- this.options.onSubmitInvalid?.(this.state.values, this)
544
+ this.options.onSubmitInvalid?.({
545
+ value: this.state.values,
546
+ formApi: this,
547
+ })
475
548
  return
476
549
  }
477
550
 
@@ -480,13 +553,16 @@ export class FormApi<TFormData, ValidatorType> {
480
553
 
481
554
  if (!this.state.isValid) {
482
555
  done()
483
- this.options.onSubmitInvalid?.(this.state.values, this)
556
+ this.options.onSubmitInvalid?.({
557
+ value: this.state.values,
558
+ formApi: this,
559
+ })
484
560
  return
485
561
  }
486
562
 
487
563
  try {
488
564
  // Run the submit code
489
- await this.options.onSubmit?.(this.state.values, this)
565
+ await this.options.onSubmit?.({ value: this.state.values, formApi: this })
490
566
 
491
567
  this.store.batch(() => {
492
568
  this.store.setState((prev) => ({ ...prev, isSubmitted: true }))
@@ -510,10 +586,17 @@ export class FormApi<TFormData, ValidatorType> {
510
586
 
511
587
  getFieldInfo = <TField extends DeepKeys<TFormData>>(
512
588
  field: TField,
513
- ): FieldInfo<TFormData, ValidatorType> => {
589
+ ): FieldInfo<TFormData, TFormValidator> => {
514
590
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
515
591
  return (this.fieldInfo[field] ||= {
516
592
  instances: {},
593
+ validationMetaMap: {
594
+ onChange: undefined,
595
+ onBlur: undefined,
596
+ onSubmit: undefined,
597
+ onMount: undefined,
598
+ onServer: undefined,
599
+ },
517
600
  })
518
601
  }
519
602
 
@@ -644,11 +727,14 @@ function getErrorMapKey(cause: ValidationCause) {
644
727
  switch (cause) {
645
728
  case 'submit':
646
729
  return 'onSubmit'
647
- case 'change':
648
- return 'onChange'
649
730
  case 'blur':
650
731
  return 'onBlur'
651
732
  case 'mount':
652
733
  return 'onMount'
734
+ case 'server':
735
+ return 'onServer'
736
+ case 'change':
737
+ default:
738
+ return 'onChange'
653
739
  }
654
740
  }