@tanstack/form-core 0.29.2 → 0.32.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.cjs +49 -20
- package/dist/cjs/FieldApi.cjs.map +1 -1
- package/dist/cjs/FieldApi.d.cts +15 -11
- package/dist/cjs/FormApi.cjs +94 -23
- package/dist/cjs/FormApi.cjs.map +1 -1
- package/dist/cjs/FormApi.d.cts +21 -10
- package/dist/cjs/types.d.cts +18 -0
- package/dist/esm/FieldApi.d.ts +15 -11
- package/dist/esm/FieldApi.js +49 -20
- package/dist/esm/FieldApi.js.map +1 -1
- package/dist/esm/FormApi.d.ts +21 -10
- package/dist/esm/FormApi.js +94 -23
- package/dist/esm/FormApi.js.map +1 -1
- package/dist/esm/types.d.ts +18 -0
- package/package.json +1 -1
- package/src/FieldApi.ts +79 -30
- package/src/FormApi.ts +165 -37
- package/src/types.ts +22 -0
package/src/FieldApi.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
2
|
import { getAsyncValidatorArray, getBy, getSyncValidatorArray } from './utils'
|
|
3
|
+
import type { FieldInfo, FieldsErrorMapFromValidator, FormApi } from './FormApi'
|
|
3
4
|
import type {
|
|
4
5
|
UpdateMetaOptions,
|
|
5
6
|
ValidationCause,
|
|
@@ -7,7 +8,6 @@ import type {
|
|
|
7
8
|
ValidationErrorMap,
|
|
8
9
|
Validator,
|
|
9
10
|
} from './types'
|
|
10
|
-
import type { FieldInfo, FormApi } from './FormApi'
|
|
11
11
|
import type { AsyncValidator, SyncValidator, Updater } from './utils'
|
|
12
12
|
import type { DeepKeys, DeepValue, NoInfer } from './util-types'
|
|
13
13
|
|
|
@@ -157,7 +157,7 @@ export interface FieldValidators<
|
|
|
157
157
|
* An optional property that takes a `ValidateFn` which is a generic of `TData` and `TParentData`.
|
|
158
158
|
* If `validatorAdapter` is passed, this may also accept a property from the respective adapter
|
|
159
159
|
*
|
|
160
|
-
* @example
|
|
160
|
+
* @example z.string().min(1) // if `zodAdapter` is passed
|
|
161
161
|
*/
|
|
162
162
|
onChange?: FieldValidateOrFn<
|
|
163
163
|
TParentData,
|
|
@@ -170,7 +170,7 @@ export interface FieldValidators<
|
|
|
170
170
|
* An optional property similar to `onChange` but async validation. If `validatorAdapter`
|
|
171
171
|
* is passed, this may also accept a property from the respective adapter
|
|
172
172
|
*
|
|
173
|
-
* @example
|
|
173
|
+
* @example z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' }) // if `zodAdapter` is passed
|
|
174
174
|
*/
|
|
175
175
|
onChangeAsync?: FieldAsyncValidateOrFn<
|
|
176
176
|
TParentData,
|
|
@@ -190,10 +190,10 @@ export interface FieldValidators<
|
|
|
190
190
|
*/
|
|
191
191
|
onChangeListenTo?: DeepKeys<TParentData>[]
|
|
192
192
|
/**
|
|
193
|
-
* An optional function, that
|
|
193
|
+
* An optional function, that runs on the blur event of input.
|
|
194
194
|
* If `validatorAdapter` is passed, this may also accept a property from the respective adapter
|
|
195
195
|
*
|
|
196
|
-
* @example
|
|
196
|
+
* @example z.string().min(1) // if `zodAdapter` is passed
|
|
197
197
|
*/
|
|
198
198
|
onBlur?: FieldValidateOrFn<
|
|
199
199
|
TParentData,
|
|
@@ -206,7 +206,7 @@ export interface FieldValidators<
|
|
|
206
206
|
* An optional property similar to `onBlur` but async validation. If `validatorAdapter`
|
|
207
207
|
* is passed, this may also accept a property from the respective adapter
|
|
208
208
|
*
|
|
209
|
-
* @example
|
|
209
|
+
* @example z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' }) // if `zodAdapter` is passed
|
|
210
210
|
*/
|
|
211
211
|
onBlurAsync?: FieldAsyncValidateOrFn<
|
|
212
212
|
TParentData,
|
|
@@ -227,10 +227,10 @@ export interface FieldValidators<
|
|
|
227
227
|
*/
|
|
228
228
|
onBlurListenTo?: DeepKeys<TParentData>[]
|
|
229
229
|
/**
|
|
230
|
-
* An optional function, that
|
|
230
|
+
* An optional function, that runs on the submit event of form.
|
|
231
231
|
* If `validatorAdapter` is passed, this may also accept a property from the respective adapter
|
|
232
232
|
*
|
|
233
|
-
* @example
|
|
233
|
+
* @example z.string().min(1) // if `zodAdapter` is passed
|
|
234
234
|
*/
|
|
235
235
|
onSubmit?: FieldValidateOrFn<
|
|
236
236
|
TParentData,
|
|
@@ -243,7 +243,7 @@ export interface FieldValidators<
|
|
|
243
243
|
* An optional property similar to `onSubmit` but async validation. If `validatorAdapter`
|
|
244
244
|
* is passed, this may also accept a property from the respective adapter
|
|
245
245
|
*
|
|
246
|
-
* @example
|
|
246
|
+
* @example z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' }) // if `zodAdapter` is passed
|
|
247
247
|
*/
|
|
248
248
|
onSubmitAsync?: FieldAsyncValidateOrFn<
|
|
249
249
|
TParentData,
|
|
@@ -335,6 +335,10 @@ export type FieldMeta = {
|
|
|
335
335
|
* A flag indicating whether the field has been touched.
|
|
336
336
|
*/
|
|
337
337
|
isTouched: boolean
|
|
338
|
+
/**
|
|
339
|
+
* A flag indicating whether the field has been blurred.
|
|
340
|
+
*/
|
|
341
|
+
isBlurred: boolean
|
|
338
342
|
/**
|
|
339
343
|
* A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`.
|
|
340
344
|
*/
|
|
@@ -456,6 +460,7 @@ export class FieldApi<
|
|
|
456
460
|
meta: this._getMeta() ?? {
|
|
457
461
|
isValidating: false,
|
|
458
462
|
isTouched: false,
|
|
463
|
+
isBlurred: false,
|
|
459
464
|
isDirty: false,
|
|
460
465
|
isPristine: true,
|
|
461
466
|
errors: [],
|
|
@@ -625,6 +630,7 @@ export class FieldApi<
|
|
|
625
630
|
({
|
|
626
631
|
isValidating: false,
|
|
627
632
|
isTouched: false,
|
|
633
|
+
isBlurred: false,
|
|
628
634
|
isDirty: false,
|
|
629
635
|
isPristine: true,
|
|
630
636
|
errors: [],
|
|
@@ -718,7 +724,10 @@ export class FieldApi<
|
|
|
718
724
|
/**
|
|
719
725
|
* @private
|
|
720
726
|
*/
|
|
721
|
-
validateSync = (
|
|
727
|
+
validateSync = (
|
|
728
|
+
cause: ValidationCause,
|
|
729
|
+
errorFromForm: ValidationErrorMap,
|
|
730
|
+
) => {
|
|
722
731
|
const validates = getSyncValidatorArray(cause, this.options)
|
|
723
732
|
|
|
724
733
|
const linkedFields = this.getLinkedFields(cause)
|
|
@@ -741,30 +750,42 @@ export class FieldApi<
|
|
|
741
750
|
field: FieldApi<any, any, any, any>,
|
|
742
751
|
validateObj: SyncValidator<any>,
|
|
743
752
|
) => {
|
|
744
|
-
const error = normalizeError(
|
|
745
|
-
field.runValidator({
|
|
746
|
-
validate: validateObj.validate,
|
|
747
|
-
value: { value: field.getValue(), fieldApi: field },
|
|
748
|
-
type: 'validate',
|
|
749
|
-
}),
|
|
750
|
-
)
|
|
751
753
|
const errorMapKey = getErrorMapKey(validateObj.cause)
|
|
754
|
+
|
|
755
|
+
const error =
|
|
756
|
+
/*
|
|
757
|
+
If `validateObj.validate` is `undefined`, then the field doesn't have
|
|
758
|
+
a validator for this event, but there still could be an error that
|
|
759
|
+
needs to be cleaned up related to the current event left by the
|
|
760
|
+
form's validator.
|
|
761
|
+
*/
|
|
762
|
+
validateObj.validate
|
|
763
|
+
? normalizeError(
|
|
764
|
+
field.runValidator({
|
|
765
|
+
validate: validateObj.validate,
|
|
766
|
+
value: { value: field.getValue(), fieldApi: field },
|
|
767
|
+
type: 'validate',
|
|
768
|
+
}),
|
|
769
|
+
)
|
|
770
|
+
: errorFromForm[errorMapKey]
|
|
771
|
+
|
|
752
772
|
if (field.state.meta.errorMap[errorMapKey] !== error) {
|
|
753
773
|
field.setMeta((prev) => ({
|
|
754
774
|
...prev,
|
|
755
775
|
errorMap: {
|
|
756
776
|
...prev.errorMap,
|
|
757
|
-
[getErrorMapKey(validateObj.cause)]:
|
|
777
|
+
[getErrorMapKey(validateObj.cause)]:
|
|
778
|
+
// Prefer the error message from the field validators if they exist
|
|
779
|
+
error ? error : errorFromForm[errorMapKey],
|
|
758
780
|
},
|
|
759
781
|
}))
|
|
760
782
|
}
|
|
761
|
-
if (error) {
|
|
783
|
+
if (error || errorFromForm[errorMapKey]) {
|
|
762
784
|
hasErrored = true
|
|
763
785
|
}
|
|
764
786
|
}
|
|
765
787
|
|
|
766
788
|
for (const validateObj of validates) {
|
|
767
|
-
if (!validateObj.validate) continue
|
|
768
789
|
validateFieldFn(this, validateObj)
|
|
769
790
|
}
|
|
770
791
|
for (const fieldValitateObj of linkedFieldValidates) {
|
|
@@ -778,6 +799,7 @@ export class FieldApi<
|
|
|
778
799
|
* to clear the error as soon as the user enters a valid value in the field
|
|
779
800
|
*/
|
|
780
801
|
const submitErrKey = getErrorMapKey('submit')
|
|
802
|
+
|
|
781
803
|
if (
|
|
782
804
|
this.state.meta.errorMap[submitErrKey] &&
|
|
783
805
|
cause !== 'submit' &&
|
|
@@ -798,9 +820,17 @@ export class FieldApi<
|
|
|
798
820
|
/**
|
|
799
821
|
* @private
|
|
800
822
|
*/
|
|
801
|
-
validateAsync = async (
|
|
823
|
+
validateAsync = async (
|
|
824
|
+
cause: ValidationCause,
|
|
825
|
+
formValidationResultPromise: Promise<
|
|
826
|
+
FieldsErrorMapFromValidator<TParentData>
|
|
827
|
+
>,
|
|
828
|
+
) => {
|
|
802
829
|
const validates = getAsyncValidatorArray(cause, this.options)
|
|
803
830
|
|
|
831
|
+
// Get the field-specific error messages that are coming from the form's validator
|
|
832
|
+
const asyncFormValidationResults = await formValidationResultPromise
|
|
833
|
+
|
|
804
834
|
const linkedFields = this.getLinkedFields(cause)
|
|
805
835
|
const linkedFieldValidates = linkedFields.reduce(
|
|
806
836
|
(acc, field) => {
|
|
@@ -835,13 +865,13 @@ export class FieldApi<
|
|
|
835
865
|
validateObj: AsyncValidator<any>,
|
|
836
866
|
promises: Promise<ValidationError | undefined>[],
|
|
837
867
|
) => {
|
|
838
|
-
const
|
|
839
|
-
const fieldValidatorMeta = field.getInfo().validationMetaMap[
|
|
868
|
+
const errorMapKey = getErrorMapKey(validateObj.cause)
|
|
869
|
+
const fieldValidatorMeta = field.getInfo().validationMetaMap[errorMapKey]
|
|
840
870
|
|
|
841
871
|
fieldValidatorMeta?.lastAbortController.abort()
|
|
842
872
|
const controller = new AbortController()
|
|
843
873
|
|
|
844
|
-
this.getInfo().validationMetaMap[
|
|
874
|
+
this.getInfo().validationMetaMap[errorMapKey] = {
|
|
845
875
|
lastAbortController: controller,
|
|
846
876
|
}
|
|
847
877
|
|
|
@@ -874,18 +904,21 @@ export class FieldApi<
|
|
|
874
904
|
}
|
|
875
905
|
if (controller.signal.aborted) return resolve(undefined)
|
|
876
906
|
const error = normalizeError(rawError)
|
|
907
|
+
const fieldErrorFromForm =
|
|
908
|
+
asyncFormValidationResults[this.name]?.[errorMapKey]
|
|
909
|
+
const fieldError = error || fieldErrorFromForm
|
|
877
910
|
field.setMeta((prev) => {
|
|
878
911
|
return {
|
|
879
912
|
...prev,
|
|
880
913
|
errorMap: {
|
|
881
914
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
882
915
|
...prev?.errorMap,
|
|
883
|
-
[
|
|
916
|
+
[errorMapKey]: fieldError,
|
|
884
917
|
},
|
|
885
918
|
}
|
|
886
919
|
})
|
|
887
920
|
|
|
888
|
-
resolve(
|
|
921
|
+
resolve(fieldError)
|
|
889
922
|
}),
|
|
890
923
|
)
|
|
891
924
|
}
|
|
@@ -925,15 +958,28 @@ export class FieldApi<
|
|
|
925
958
|
validate = (
|
|
926
959
|
cause: ValidationCause,
|
|
927
960
|
): ValidationError[] | Promise<ValidationError[]> => {
|
|
928
|
-
// If the field is pristine
|
|
961
|
+
// If the field is pristine, do not validate
|
|
929
962
|
if (!this.state.meta.isTouched) return []
|
|
930
963
|
|
|
964
|
+
let validationErrorFromForm: ValidationErrorMap = {}
|
|
965
|
+
let formValidationResultPromise: Promise<
|
|
966
|
+
FieldsErrorMapFromValidator<TParentData>
|
|
967
|
+
> = Promise.resolve({})
|
|
968
|
+
|
|
931
969
|
try {
|
|
932
|
-
this.form.validate(cause)
|
|
970
|
+
const formValidationResult = this.form.validate(cause)
|
|
971
|
+
if (formValidationResult instanceof Promise) {
|
|
972
|
+
formValidationResultPromise = formValidationResult
|
|
973
|
+
} else {
|
|
974
|
+
const fieldErrorFromForm = formValidationResult[this.name]
|
|
975
|
+
if (fieldErrorFromForm) {
|
|
976
|
+
validationErrorFromForm = fieldErrorFromForm
|
|
977
|
+
}
|
|
978
|
+
}
|
|
933
979
|
} catch (_) {}
|
|
934
980
|
|
|
935
981
|
// Attempt to sync validate first
|
|
936
|
-
const { hasErrored } = this.validateSync(cause)
|
|
982
|
+
const { hasErrored } = this.validateSync(cause, validationErrorFromForm)
|
|
937
983
|
|
|
938
984
|
if (hasErrored && !this.options.asyncAlways) {
|
|
939
985
|
this.getInfo().validationMetaMap[
|
|
@@ -942,7 +988,7 @@ export class FieldApi<
|
|
|
942
988
|
return this.state.meta.errors
|
|
943
989
|
}
|
|
944
990
|
// No error? Attempt async validation
|
|
945
|
-
return this.validateAsync(cause)
|
|
991
|
+
return this.validateAsync(cause, formValidationResultPromise)
|
|
946
992
|
}
|
|
947
993
|
|
|
948
994
|
/**
|
|
@@ -961,6 +1007,9 @@ export class FieldApi<
|
|
|
961
1007
|
this.setMeta((prev) => ({ ...prev, isTouched: true }))
|
|
962
1008
|
this.validate('change')
|
|
963
1009
|
}
|
|
1010
|
+
if (!this.state.meta.isBlurred) {
|
|
1011
|
+
this.setMeta((prev) => ({ ...prev, isBlurred: true }))
|
|
1012
|
+
}
|
|
964
1013
|
this.validate('blur')
|
|
965
1014
|
}
|
|
966
1015
|
|
package/src/FormApi.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Updater } from './utils'
|
|
|
12
12
|
import type { DeepKeys, DeepValue } from './util-types'
|
|
13
13
|
import type { FieldApi, FieldMeta } from './FieldApi'
|
|
14
14
|
import type {
|
|
15
|
+
FormValidationError,
|
|
15
16
|
UpdateMetaOptions,
|
|
16
17
|
ValidationCause,
|
|
17
18
|
ValidationError,
|
|
@@ -20,16 +21,17 @@ import type {
|
|
|
20
21
|
Validator,
|
|
21
22
|
} from './types'
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
export type FieldsErrorMapFromValidator<TFormData> = Partial<
|
|
25
|
+
Record<DeepKeys<TFormData>, ValidationErrorMap>
|
|
26
|
+
>
|
|
27
|
+
|
|
26
28
|
export type FormValidateFn<
|
|
27
29
|
TFormData,
|
|
28
30
|
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
|
|
29
31
|
> = (props: {
|
|
30
32
|
value: TFormData
|
|
31
33
|
formApi: FormApi<TFormData, TFormValidator>
|
|
32
|
-
}) =>
|
|
34
|
+
}) => FormValidationError<TFormData>
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* @private
|
|
@@ -52,7 +54,22 @@ export type FormValidateAsyncFn<
|
|
|
52
54
|
value: TFormData
|
|
53
55
|
formApi: FormApi<TFormData, TFormValidator>
|
|
54
56
|
signal: AbortSignal
|
|
55
|
-
}) =>
|
|
57
|
+
}) => FormValidationError<TFormData> | Promise<FormValidationError<TFormData>>
|
|
58
|
+
|
|
59
|
+
export type FormValidator<TFormData, TType, TFn = unknown> = {
|
|
60
|
+
validate(options: { value: TType }, fn: TFn): ValidationError
|
|
61
|
+
validateAsync(
|
|
62
|
+
options: { value: TType },
|
|
63
|
+
fn: TFn,
|
|
64
|
+
): Promise<FormValidationError<TFormData>>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type ValidationPromiseResult<TFormData> =
|
|
68
|
+
| {
|
|
69
|
+
fieldErrors: Partial<Record<DeepKeys<TFormData>, ValidationError>>
|
|
70
|
+
errorMapKey: ValidationErrorMapKeys
|
|
71
|
+
}
|
|
72
|
+
| undefined
|
|
56
73
|
|
|
57
74
|
/**
|
|
58
75
|
* @private
|
|
@@ -86,11 +103,11 @@ export interface FormValidators<
|
|
|
86
103
|
*/
|
|
87
104
|
onChangeAsyncDebounceMs?: number
|
|
88
105
|
/**
|
|
89
|
-
* Optional function that validates the form data when a field loses focus, returns a
|
|
106
|
+
* Optional function that validates the form data when a field loses focus, returns a `FormValidationError`
|
|
90
107
|
*/
|
|
91
108
|
onBlur?: FormValidateOrFn<TFormData, TFormValidator>
|
|
92
109
|
/**
|
|
93
|
-
* Optional onBlur asynchronous validation method for when a field loses focus
|
|
110
|
+
* Optional onBlur asynchronous validation method for when a field loses focus returns a ` FormValidationError` or a promise of `Promise<FormValidationError>`
|
|
94
111
|
*/
|
|
95
112
|
onBlurAsync?: FormAsyncValidateOrFn<TFormData, TFormValidator>
|
|
96
113
|
/**
|
|
@@ -242,6 +259,10 @@ export type FormState<TFormData> = {
|
|
|
242
259
|
* A boolean indicating if any of the form fields have been touched.
|
|
243
260
|
*/
|
|
244
261
|
isTouched: boolean
|
|
262
|
+
/**
|
|
263
|
+
* A boolean indicating if any of the form fields have been blurred.
|
|
264
|
+
*/
|
|
265
|
+
isBlurred: boolean
|
|
245
266
|
/**
|
|
246
267
|
* A boolean indicating if any of the form's fields' values have been modified by the user. `True` if the user have modified at least one of the fields. Opposite of `isPristine`.
|
|
247
268
|
*/
|
|
@@ -288,6 +309,7 @@ function getDefaultFormState<TFormData>(
|
|
|
288
309
|
isSubmitted: defaultState.isSubmitted ?? false,
|
|
289
310
|
isSubmitting: defaultState.isSubmitting ?? false,
|
|
290
311
|
isTouched: defaultState.isTouched ?? false,
|
|
312
|
+
isBlurred: defaultState.isBlurred ?? false,
|
|
291
313
|
isPristine: defaultState.isPristine ?? true,
|
|
292
314
|
isDirty: defaultState.isDirty ?? false,
|
|
293
315
|
isValid: defaultState.isValid ?? false,
|
|
@@ -371,6 +393,7 @@ export class FormApi<
|
|
|
371
393
|
)
|
|
372
394
|
|
|
373
395
|
const isTouched = fieldMetaValues.some((field) => field?.isTouched)
|
|
396
|
+
const isBlurred = fieldMetaValues.some((field) => field?.isBlurred)
|
|
374
397
|
|
|
375
398
|
const isDirty = fieldMetaValues.some((field) => field?.isDirty)
|
|
376
399
|
const isPristine = !isDirty
|
|
@@ -393,6 +416,7 @@ export class FormApi<
|
|
|
393
416
|
isValid,
|
|
394
417
|
canSubmit,
|
|
395
418
|
isTouched,
|
|
419
|
+
isBlurred,
|
|
396
420
|
isPristine,
|
|
397
421
|
isDirty,
|
|
398
422
|
}
|
|
@@ -536,6 +560,12 @@ export class FormApi<
|
|
|
536
560
|
// Mark them as touched
|
|
537
561
|
field.instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
|
538
562
|
}
|
|
563
|
+
|
|
564
|
+
// If any fields are not blurred
|
|
565
|
+
if (!field.instance.state.meta.isBlurred) {
|
|
566
|
+
// Mark them as blurred
|
|
567
|
+
field.instance.setMeta((prev) => ({ ...prev, isBlurred: true }))
|
|
568
|
+
}
|
|
539
569
|
})
|
|
540
570
|
})
|
|
541
571
|
|
|
@@ -599,6 +629,12 @@ export class FormApi<
|
|
|
599
629
|
fieldInstance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
|
600
630
|
}
|
|
601
631
|
|
|
632
|
+
// If the field is not blurred (same logic as in validateAllFields)
|
|
633
|
+
if (!fieldInstance.state.meta.isBlurred) {
|
|
634
|
+
// Mark it as blurred
|
|
635
|
+
fieldInstance.setMeta((prev) => ({ ...prev, isBlurred: true }))
|
|
636
|
+
}
|
|
637
|
+
|
|
602
638
|
return fieldInstance.validate(cause)
|
|
603
639
|
}
|
|
604
640
|
|
|
@@ -606,35 +642,68 @@ export class FormApi<
|
|
|
606
642
|
* TODO: This code is copied from FieldApi, we should refactor to share
|
|
607
643
|
* @private
|
|
608
644
|
*/
|
|
609
|
-
validateSync = (
|
|
645
|
+
validateSync = (
|
|
646
|
+
cause: ValidationCause,
|
|
647
|
+
): {
|
|
648
|
+
hasErrored: boolean
|
|
649
|
+
fieldsErrorMap: FieldsErrorMapFromValidator<TFormData>
|
|
650
|
+
} => {
|
|
610
651
|
const validates = getSyncValidatorArray(cause, this.options)
|
|
611
652
|
let hasErrored = false as boolean
|
|
612
653
|
|
|
654
|
+
const fieldsErrorMap: FieldsErrorMapFromValidator<TFormData> = {}
|
|
655
|
+
|
|
613
656
|
this.store.batch(() => {
|
|
614
657
|
for (const validateObj of validates) {
|
|
615
658
|
if (!validateObj.validate) continue
|
|
616
659
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
value:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
)
|
|
660
|
+
const rawError = this.runValidator({
|
|
661
|
+
validate: validateObj.validate,
|
|
662
|
+
value: {
|
|
663
|
+
value: this.state.values,
|
|
664
|
+
formApi: this,
|
|
665
|
+
},
|
|
666
|
+
type: 'validate',
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
const { formError, fieldErrors } = normalizeError<TFormData>(rawError)
|
|
670
|
+
|
|
627
671
|
const errorMapKey = getErrorMapKey(validateObj.cause)
|
|
628
|
-
|
|
672
|
+
|
|
673
|
+
if (fieldErrors) {
|
|
674
|
+
for (const [field, fieldError] of Object.entries(fieldErrors)) {
|
|
675
|
+
const oldErrorMap =
|
|
676
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] || {}
|
|
677
|
+
const newErrorMap = {
|
|
678
|
+
...oldErrorMap,
|
|
679
|
+
[errorMapKey]: fieldError,
|
|
680
|
+
}
|
|
681
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] = newErrorMap
|
|
682
|
+
|
|
683
|
+
const fieldMeta = this.getFieldMeta(field as DeepKeys<TFormData>)
|
|
684
|
+
if (fieldMeta && fieldMeta.errorMap[errorMapKey] !== fieldError) {
|
|
685
|
+
this.setFieldMeta(field as DeepKeys<TFormData>, (prev) => ({
|
|
686
|
+
...prev,
|
|
687
|
+
errorMap: {
|
|
688
|
+
...prev.errorMap,
|
|
689
|
+
[errorMapKey]: fieldError,
|
|
690
|
+
},
|
|
691
|
+
}))
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (this.state.errorMap[errorMapKey] !== formError) {
|
|
629
697
|
this.store.setState((prev) => ({
|
|
630
698
|
...prev,
|
|
631
699
|
errorMap: {
|
|
632
700
|
...prev.errorMap,
|
|
633
|
-
[errorMapKey]:
|
|
701
|
+
[errorMapKey]: formError,
|
|
634
702
|
},
|
|
635
703
|
}))
|
|
636
704
|
}
|
|
637
|
-
|
|
705
|
+
|
|
706
|
+
if (formError || fieldErrors) {
|
|
638
707
|
hasErrored = true
|
|
639
708
|
}
|
|
640
709
|
}
|
|
@@ -659,7 +728,7 @@ export class FormApi<
|
|
|
659
728
|
}))
|
|
660
729
|
}
|
|
661
730
|
|
|
662
|
-
return { hasErrored }
|
|
731
|
+
return { hasErrored, fieldsErrorMap }
|
|
663
732
|
}
|
|
664
733
|
|
|
665
734
|
/**
|
|
@@ -667,7 +736,7 @@ export class FormApi<
|
|
|
667
736
|
*/
|
|
668
737
|
validateAsync = async (
|
|
669
738
|
cause: ValidationCause,
|
|
670
|
-
): Promise<
|
|
739
|
+
): Promise<FieldsErrorMapFromValidator<TFormData>> => {
|
|
671
740
|
const validates = getAsyncValidatorArray(cause, this.options)
|
|
672
741
|
|
|
673
742
|
if (!this.state.isFormValidating) {
|
|
@@ -678,7 +747,11 @@ export class FormApi<
|
|
|
678
747
|
* We have to use a for loop and generate our promises this way, otherwise it won't be sync
|
|
679
748
|
* when there are no validators needed to be run
|
|
680
749
|
*/
|
|
681
|
-
const promises: Promise<
|
|
750
|
+
const promises: Promise<ValidationPromiseResult<TFormData>>[] = []
|
|
751
|
+
|
|
752
|
+
let fieldErrors:
|
|
753
|
+
| Partial<Record<DeepKeys<TFormData>, ValidationError>>
|
|
754
|
+
| undefined
|
|
682
755
|
|
|
683
756
|
for (const validateObj of validates) {
|
|
684
757
|
if (!validateObj.validate) continue
|
|
@@ -693,7 +766,7 @@ export class FormApi<
|
|
|
693
766
|
}
|
|
694
767
|
|
|
695
768
|
promises.push(
|
|
696
|
-
new Promise<
|
|
769
|
+
new Promise<ValidationPromiseResult<TFormData>>(async (resolve) => {
|
|
697
770
|
let rawError!: ValidationError | undefined
|
|
698
771
|
try {
|
|
699
772
|
rawError = await new Promise((rawResolve, rawReject) => {
|
|
@@ -719,23 +792,65 @@ export class FormApi<
|
|
|
719
792
|
} catch (e: unknown) {
|
|
720
793
|
rawError = e as ValidationError
|
|
721
794
|
}
|
|
722
|
-
const
|
|
795
|
+
const { formError, fieldErrors: fieldErrorsFromNormalizeError } =
|
|
796
|
+
normalizeError<TFormData>(rawError)
|
|
797
|
+
|
|
798
|
+
if (fieldErrorsFromNormalizeError) {
|
|
799
|
+
fieldErrors = fieldErrors
|
|
800
|
+
? { ...fieldErrors, ...fieldErrorsFromNormalizeError }
|
|
801
|
+
: fieldErrorsFromNormalizeError
|
|
802
|
+
}
|
|
803
|
+
const errorMapKey = getErrorMapKey(validateObj.cause)
|
|
804
|
+
|
|
805
|
+
if (fieldErrors) {
|
|
806
|
+
for (const [field, fieldError] of Object.entries(fieldErrors)) {
|
|
807
|
+
const fieldMeta = this.getFieldMeta(field as DeepKeys<TFormData>)
|
|
808
|
+
if (fieldMeta && fieldMeta.errorMap[errorMapKey] !== fieldError) {
|
|
809
|
+
this.setFieldMeta(field as DeepKeys<TFormData>, (prev) => ({
|
|
810
|
+
...prev,
|
|
811
|
+
errorMap: {
|
|
812
|
+
...prev.errorMap,
|
|
813
|
+
[errorMapKey]: fieldError,
|
|
814
|
+
},
|
|
815
|
+
}))
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
723
819
|
this.store.setState((prev) => ({
|
|
724
820
|
...prev,
|
|
725
821
|
errorMap: {
|
|
726
822
|
...prev.errorMap,
|
|
727
|
-
[
|
|
823
|
+
[errorMapKey]: formError,
|
|
728
824
|
},
|
|
729
825
|
}))
|
|
730
826
|
|
|
731
|
-
resolve(
|
|
827
|
+
resolve(fieldErrors ? { fieldErrors, errorMapKey } : undefined)
|
|
732
828
|
}),
|
|
733
829
|
)
|
|
734
830
|
}
|
|
735
831
|
|
|
736
|
-
let results:
|
|
832
|
+
let results: ValidationPromiseResult<TFormData>[] = []
|
|
833
|
+
|
|
834
|
+
const fieldsErrorMap: FieldsErrorMapFromValidator<TFormData> = {}
|
|
737
835
|
if (promises.length) {
|
|
738
836
|
results = await Promise.all(promises)
|
|
837
|
+
for (const fieldValidationResult of results) {
|
|
838
|
+
if (fieldValidationResult?.fieldErrors) {
|
|
839
|
+
const { errorMapKey } = fieldValidationResult
|
|
840
|
+
|
|
841
|
+
for (const [field, fieldError] of Object.entries(
|
|
842
|
+
fieldValidationResult.fieldErrors,
|
|
843
|
+
)) {
|
|
844
|
+
const oldErrorMap =
|
|
845
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] || {}
|
|
846
|
+
const newErrorMap = {
|
|
847
|
+
...oldErrorMap,
|
|
848
|
+
[errorMapKey]: fieldError,
|
|
849
|
+
}
|
|
850
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] = newErrorMap
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
739
854
|
}
|
|
740
855
|
|
|
741
856
|
this.store.setState((prev) => ({
|
|
@@ -743,7 +858,7 @@ export class FormApi<
|
|
|
743
858
|
isFormValidating: false,
|
|
744
859
|
}))
|
|
745
860
|
|
|
746
|
-
return
|
|
861
|
+
return fieldsErrorMap
|
|
747
862
|
}
|
|
748
863
|
|
|
749
864
|
/**
|
|
@@ -751,12 +866,14 @@ export class FormApi<
|
|
|
751
866
|
*/
|
|
752
867
|
validate = (
|
|
753
868
|
cause: ValidationCause,
|
|
754
|
-
):
|
|
869
|
+
):
|
|
870
|
+
| FieldsErrorMapFromValidator<TFormData>
|
|
871
|
+
| Promise<FieldsErrorMapFromValidator<TFormData>> => {
|
|
755
872
|
// Attempt to sync validate first
|
|
756
|
-
const { hasErrored } = this.validateSync(cause)
|
|
873
|
+
const { hasErrored, fieldsErrorMap } = this.validateSync(cause)
|
|
757
874
|
|
|
758
875
|
if (hasErrored && !this.options.asyncAlways) {
|
|
759
|
-
return
|
|
876
|
+
return fieldsErrorMap
|
|
760
877
|
}
|
|
761
878
|
|
|
762
879
|
// No error? Attempt async validation
|
|
@@ -885,6 +1002,7 @@ export class FormApi<
|
|
|
885
1002
|
acc[fieldKey] = {
|
|
886
1003
|
isValidating: false,
|
|
887
1004
|
isTouched: false,
|
|
1005
|
+
isBlurred: false,
|
|
888
1006
|
isDirty: false,
|
|
889
1007
|
isPristine: true,
|
|
890
1008
|
errors: [],
|
|
@@ -911,6 +1029,7 @@ export class FormApi<
|
|
|
911
1029
|
this.setFieldMeta(field, (prev) => ({
|
|
912
1030
|
...prev,
|
|
913
1031
|
isTouched: true,
|
|
1032
|
+
isBlurred: true,
|
|
914
1033
|
isDirty: true,
|
|
915
1034
|
}))
|
|
916
1035
|
}
|
|
@@ -1109,16 +1228,25 @@ export class FormApi<
|
|
|
1109
1228
|
}
|
|
1110
1229
|
}
|
|
1111
1230
|
|
|
1112
|
-
function normalizeError(rawError?:
|
|
1231
|
+
function normalizeError<TFormData>(rawError?: FormValidationError<TFormData>): {
|
|
1232
|
+
formError: ValidationError
|
|
1233
|
+
fieldErrors?: Partial<Record<DeepKeys<TFormData>, ValidationError>>
|
|
1234
|
+
} {
|
|
1113
1235
|
if (rawError) {
|
|
1236
|
+
if (typeof rawError === 'object') {
|
|
1237
|
+
const formError = normalizeError(rawError.form).formError
|
|
1238
|
+
const fieldErrors = rawError.fields
|
|
1239
|
+
return { formError, fieldErrors }
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1114
1242
|
if (typeof rawError !== 'string') {
|
|
1115
|
-
return 'Invalid Form Values'
|
|
1243
|
+
return { formError: 'Invalid Form Values' }
|
|
1116
1244
|
}
|
|
1117
1245
|
|
|
1118
|
-
return rawError
|
|
1246
|
+
return { formError: rawError }
|
|
1119
1247
|
}
|
|
1120
1248
|
|
|
1121
|
-
return undefined
|
|
1249
|
+
return { formError: undefined }
|
|
1122
1250
|
}
|
|
1123
1251
|
|
|
1124
1252
|
function getErrorMapKey(cause: ValidationCause) {
|