@tanstack/form-core 0.10.3 → 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.
- package/build/legacy/FieldApi.cjs +91 -118
- package/build/legacy/FieldApi.cjs.map +1 -1
- package/build/legacy/FieldApi.d.cts +1 -2
- package/build/legacy/FieldApi.d.ts +1 -2
- package/build/legacy/FieldApi.js +91 -118
- package/build/legacy/FieldApi.js.map +1 -1
- package/build/legacy/FormApi.cjs +125 -118
- package/build/legacy/FormApi.cjs.map +1 -1
- package/build/legacy/FormApi.d.cts +1 -2
- package/build/legacy/FormApi.d.ts +1 -2
- package/build/legacy/FormApi.js +127 -118
- package/build/legacy/FormApi.js.map +1 -1
- package/build/legacy/index.d.cts +160 -73
- package/build/legacy/index.d.ts +160 -73
- package/build/legacy/types.cjs.map +1 -1
- package/build/legacy/types.d.cts +12 -3
- package/build/legacy/types.d.ts +12 -3
- package/build/legacy/utils.cjs +55 -0
- package/build/legacy/utils.cjs.map +1 -1
- package/build/legacy/utils.d.cts +3 -37
- package/build/legacy/utils.d.ts +3 -37
- package/build/legacy/utils.js +53 -0
- package/build/legacy/utils.js.map +1 -1
- package/build/modern/FieldApi.cjs +91 -116
- package/build/modern/FieldApi.cjs.map +1 -1
- package/build/modern/FieldApi.d.cts +1 -2
- package/build/modern/FieldApi.d.ts +1 -2
- package/build/modern/FieldApi.js +91 -116
- package/build/modern/FieldApi.js.map +1 -1
- package/build/modern/FormApi.cjs +125 -117
- package/build/modern/FormApi.cjs.map +1 -1
- package/build/modern/FormApi.d.cts +1 -2
- package/build/modern/FormApi.d.ts +1 -2
- package/build/modern/FormApi.js +127 -117
- package/build/modern/FormApi.js.map +1 -1
- package/build/modern/index.d.cts +160 -73
- package/build/modern/index.d.ts +160 -73
- package/build/modern/types.cjs.map +1 -1
- package/build/modern/types.d.cts +12 -3
- package/build/modern/types.d.ts +12 -3
- package/build/modern/utils.cjs +55 -0
- package/build/modern/utils.cjs.map +1 -1
- package/build/modern/utils.d.cts +3 -37
- package/build/modern/utils.d.ts +3 -37
- package/build/modern/utils.js +53 -0
- package/build/modern/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/FieldApi.ts +308 -231
- package/src/FormApi.ts +259 -209
- package/src/tests/FieldApi.spec.ts +135 -48
- package/src/tests/FieldApi.test-d.ts +10 -6
- package/src/tests/FormApi.spec.ts +170 -62
- package/src/tests/utils.ts +1 -1
- package/src/types.ts +10 -2
- 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
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
value:
|
|
24
|
-
|
|
25
|
-
) => ValidationError
|
|
26
|
-
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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?:
|
|
38
|
-
onBlurAsync?:
|
|
60
|
+
onBlur?: FormValidateOrFn<TFormData, TFormValidator>
|
|
61
|
+
onBlurAsync?: FormAsyncValidateOrFn<TFormData, TFormValidator>
|
|
39
62
|
onBlurAsyncDebounceMs?: number
|
|
40
|
-
onSubmit?:
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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<
|
|
69
|
-
values:
|
|
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<
|
|
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<
|
|
91
|
-
defaultState: Partial<FormState<
|
|
92
|
-
): FormState<
|
|
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
|
-
|
|
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<
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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,
|
|
267
|
+
update = (options?: FormOptions<TFormData, TFormValidator>) => {
|
|
211
268
|
if (!options) return
|
|
212
269
|
|
|
213
270
|
this.store.batch(() => {
|
|
@@ -253,7 +310,7 @@ 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,
|
|
313
|
+
Object.values(this.fieldInfo) as FieldInfo<any, TFormValidator>[]
|
|
257
314
|
).forEach((field) => {
|
|
258
315
|
Object.values(field.instances).forEach((instance) => {
|
|
259
316
|
// Validate the field
|
|
@@ -269,174 +326,155 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
269
326
|
})
|
|
270
327
|
})
|
|
271
328
|
|
|
272
|
-
|
|
329
|
+
const fieldErrorMapMap = await Promise.all(fieldValidationPromises)
|
|
330
|
+
return fieldErrorMapMap.flat()
|
|
273
331
|
}
|
|
274
332
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
[
|
|
382
|
+
[submitErrKey]: undefined,
|
|
303
383
|
},
|
|
304
384
|
}))
|
|
305
385
|
}
|
|
306
386
|
|
|
307
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
validationAsyncCount === this.validationMeta.validationAsyncCount
|
|
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>[] = []
|
|
362
404
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
})
|
|
368
|
-
}
|
|
405
|
+
for (const validateObj of validates) {
|
|
406
|
+
if (!validateObj.validate) continue
|
|
407
|
+
const key = getErrorMapKey(validateObj.cause)
|
|
408
|
+
const fieldValidatorMeta = this.state.validationMetaMap[key]
|
|
369
409
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
410
|
+
fieldValidatorMeta?.lastAbortController.abort()
|
|
411
|
+
// Sorry Safari 12
|
|
412
|
+
// eslint-disable-next-line compat/compat
|
|
413
|
+
const controller = new AbortController()
|
|
373
414
|
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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?.(
|
|
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?.(
|
|
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,
|
|
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
|
|