@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.
- package/dist/cjs/FieldApi.d.ts +95 -0
- package/dist/cjs/FormApi.d.ts +118 -0
- package/dist/cjs/index.cjs +926 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +5 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +926 -0
- package/dist/cjs/mergeForm.d.ts +4 -0
- package/dist/cjs/tests/FieldApi.spec.d.ts +1 -0
- package/dist/cjs/tests/FieldApi.test-d.d.ts +1 -0
- package/dist/cjs/tests/FormApi.spec.d.ts +1 -0
- package/dist/cjs/tests/mutateMergeDeep.spec.d.ts +1 -0
- package/dist/cjs/tests/utils.d.ts +1 -0
- package/dist/cjs/tests/utils.spec.d.ts +1 -0
- package/dist/cjs/types.d.ts +14 -0
- package/dist/cjs/utils.d.ts +57 -0
- package/dist/mjs/FieldApi.d.ts +95 -0
- package/dist/mjs/FormApi.d.ts +118 -0
- package/dist/mjs/index.d.mts +5 -0
- package/dist/mjs/index.d.ts +5 -0
- package/dist/mjs/index.js +926 -0
- package/dist/mjs/index.mjs +926 -0
- package/dist/mjs/index.mjs.map +1 -0
- package/dist/mjs/mergeForm.d.ts +4 -0
- package/dist/mjs/tests/FieldApi.spec.d.ts +1 -0
- package/dist/mjs/tests/FieldApi.test-d.d.ts +1 -0
- package/dist/mjs/tests/FormApi.spec.d.ts +1 -0
- package/dist/mjs/tests/mutateMergeDeep.spec.d.ts +1 -0
- package/dist/mjs/tests/utils.d.ts +1 -0
- package/dist/mjs/tests/utils.spec.d.ts +1 -0
- package/dist/mjs/types.d.ts +14 -0
- package/dist/mjs/utils.d.ts +57 -0
- package/package.json +16 -21
- package/src/FieldApi.ts +328 -236
- package/src/FormApi.ts +302 -216
- package/src/index.ts +1 -0
- package/src/mergeForm.ts +42 -0
- package/src/tests/FieldApi.spec.ts +135 -48
- package/src/tests/FieldApi.test-d.ts +10 -6
- package/src/tests/FormApi.spec.ts +171 -62
- package/src/tests/mutateMergeDeep.spec.ts +32 -0
- package/src/tests/utils.ts +1 -1
- package/src/types.ts +11 -2
- package/src/utils.ts +137 -14
- package/build/legacy/FieldApi.cjs +0 -340
- package/build/legacy/FieldApi.cjs.map +0 -1
- package/build/legacy/FieldApi.d.cts +0 -4
- package/build/legacy/FieldApi.d.ts +0 -4
- package/build/legacy/FieldApi.js +0 -315
- package/build/legacy/FieldApi.js.map +0 -1
- package/build/legacy/FormApi.cjs +0 -438
- package/build/legacy/FormApi.cjs.map +0 -1
- package/build/legacy/FormApi.d.cts +0 -4
- package/build/legacy/FormApi.d.ts +0 -4
- package/build/legacy/FormApi.js +0 -419
- package/build/legacy/FormApi.js.map +0 -1
- package/build/legacy/index.cjs +0 -31
- package/build/legacy/index.cjs.map +0 -1
- package/build/legacy/index.d.cts +0 -170
- package/build/legacy/index.d.ts +0 -170
- package/build/legacy/index.js +0 -6
- package/build/legacy/index.js.map +0 -1
- package/build/legacy/types.cjs +0 -19
- package/build/legacy/types.cjs.map +0 -1
- package/build/legacy/types.d.cts +0 -7
- package/build/legacy/types.d.ts +0 -7
- package/build/legacy/types.js +0 -1
- package/build/legacy/types.js.map +0 -1
- package/build/legacy/utils.cjs +0 -132
- package/build/legacy/utils.cjs.map +0 -1
- package/build/legacy/utils.d.cts +0 -37
- package/build/legacy/utils.d.ts +0 -37
- package/build/legacy/utils.js +0 -103
- package/build/legacy/utils.js.map +0 -1
- package/build/modern/FieldApi.cjs +0 -337
- package/build/modern/FieldApi.cjs.map +0 -1
- package/build/modern/FieldApi.d.cts +0 -4
- package/build/modern/FieldApi.d.ts +0 -4
- package/build/modern/FieldApi.js +0 -312
- package/build/modern/FieldApi.js.map +0 -1
- package/build/modern/FormApi.cjs +0 -431
- package/build/modern/FormApi.cjs.map +0 -1
- package/build/modern/FormApi.d.cts +0 -4
- package/build/modern/FormApi.d.ts +0 -4
- package/build/modern/FormApi.js +0 -412
- package/build/modern/FormApi.js.map +0 -1
- package/build/modern/index.cjs +0 -31
- package/build/modern/index.cjs.map +0 -1
- package/build/modern/index.d.cts +0 -170
- package/build/modern/index.d.ts +0 -170
- package/build/modern/index.js +0 -6
- package/build/modern/index.js.map +0 -1
- package/build/modern/types.cjs +0 -19
- package/build/modern/types.cjs.map +0 -1
- package/build/modern/types.d.cts +0 -7
- package/build/modern/types.d.ts +0 -7
- package/build/modern/types.js +0 -1
- package/build/modern/types.js.map +0 -1
- package/build/modern/utils.cjs +0 -132
- package/build/modern/utils.cjs.map +0 -1
- package/build/modern/utils.d.cts +0 -37
- package/build/modern/utils.d.ts +0 -37
- package/build/modern/utils.js +0 -103
- 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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
99
|
+
export type ValidationMeta = {
|
|
100
|
+
lastAbortController: AbortController
|
|
101
|
+
}
|
|
63
102
|
|
|
64
|
-
export type
|
|
65
|
-
|
|
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<
|
|
69
|
-
values:
|
|
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<
|
|
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<
|
|
91
|
-
defaultState: Partial<FormState<
|
|
92
|
-
): FormState<
|
|
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
|
-
|
|
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<
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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,
|
|
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 !==
|
|
305
|
+
options.defaultValues !== oldOptions.defaultValues &&
|
|
217
306
|
!this.state.isTouched
|
|
218
307
|
|
|
219
308
|
const shouldUpdateState =
|
|
220
|
-
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,
|
|
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
|
-
|
|
359
|
+
const fieldErrorMapMap = await Promise.all(fieldValidationPromises)
|
|
360
|
+
return fieldErrorMapMap.flat()
|
|
273
361
|
}
|
|
274
362
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
[
|
|
412
|
+
[submitErrKey]: undefined,
|
|
303
413
|
},
|
|
304
414
|
}))
|
|
305
415
|
}
|
|
306
416
|
|
|
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
|
-
}))
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
440
|
+
fieldValidatorMeta?.lastAbortController.abort()
|
|
441
|
+
const controller = new AbortController()
|
|
373
442
|
|
|
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
|
-
)
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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?.(
|
|
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?.(
|
|
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,
|
|
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
|
}
|