@tanstack/form-core 0.28.0 → 0.30.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 +44 -20
- package/dist/cjs/FieldApi.cjs.map +1 -1
- package/dist/cjs/FieldApi.d.cts +11 -12
- package/dist/cjs/FormApi.cjs +83 -23
- package/dist/cjs/FormApi.cjs.map +1 -1
- package/dist/cjs/FormApi.d.cts +17 -11
- package/dist/cjs/formOptions.d.cts +0 -1
- package/dist/cjs/mergeForm.d.cts +0 -1
- package/dist/cjs/types.d.cts +25 -0
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/FieldApi.d.ts +11 -12
- package/dist/esm/FieldApi.js +44 -20
- package/dist/esm/FieldApi.js.map +1 -1
- package/dist/esm/FormApi.d.ts +17 -11
- package/dist/esm/FormApi.js +83 -23
- package/dist/esm/FormApi.js.map +1 -1
- package/dist/esm/formOptions.d.ts +0 -1
- package/dist/esm/mergeForm.d.ts +0 -1
- package/dist/esm/types.d.ts +25 -0
- package/dist/esm/utils.d.ts +0 -1
- package/package.json +1 -1
- package/src/FieldApi.ts +70 -30
- package/src/FormApi.ts +144 -37
- package/src/types.ts +30 -0
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
|
/**
|
|
@@ -606,35 +623,68 @@ export class FormApi<
|
|
|
606
623
|
* TODO: This code is copied from FieldApi, we should refactor to share
|
|
607
624
|
* @private
|
|
608
625
|
*/
|
|
609
|
-
validateSync = (
|
|
626
|
+
validateSync = (
|
|
627
|
+
cause: ValidationCause,
|
|
628
|
+
): {
|
|
629
|
+
hasErrored: boolean
|
|
630
|
+
fieldsErrorMap: FieldsErrorMapFromValidator<TFormData>
|
|
631
|
+
} => {
|
|
610
632
|
const validates = getSyncValidatorArray(cause, this.options)
|
|
611
633
|
let hasErrored = false as boolean
|
|
612
634
|
|
|
635
|
+
const fieldsErrorMap: FieldsErrorMapFromValidator<TFormData> = {}
|
|
636
|
+
|
|
613
637
|
this.store.batch(() => {
|
|
614
638
|
for (const validateObj of validates) {
|
|
615
639
|
if (!validateObj.validate) continue
|
|
616
640
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
value:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
)
|
|
641
|
+
const rawError = this.runValidator({
|
|
642
|
+
validate: validateObj.validate,
|
|
643
|
+
value: {
|
|
644
|
+
value: this.state.values,
|
|
645
|
+
formApi: this,
|
|
646
|
+
},
|
|
647
|
+
type: 'validate',
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
const { formError, fieldErrors } = normalizeError<TFormData>(rawError)
|
|
651
|
+
|
|
627
652
|
const errorMapKey = getErrorMapKey(validateObj.cause)
|
|
628
|
-
|
|
653
|
+
|
|
654
|
+
if (fieldErrors) {
|
|
655
|
+
for (const [field, fieldError] of Object.entries(fieldErrors)) {
|
|
656
|
+
const oldErrorMap =
|
|
657
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] || {}
|
|
658
|
+
const newErrorMap = {
|
|
659
|
+
...oldErrorMap,
|
|
660
|
+
[errorMapKey]: fieldError,
|
|
661
|
+
}
|
|
662
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] = newErrorMap
|
|
663
|
+
|
|
664
|
+
const fieldMeta = this.getFieldMeta(field as DeepKeys<TFormData>)
|
|
665
|
+
if (fieldMeta && fieldMeta.errorMap[errorMapKey] !== fieldError) {
|
|
666
|
+
this.setFieldMeta(field as DeepKeys<TFormData>, (prev) => ({
|
|
667
|
+
...prev,
|
|
668
|
+
errorMap: {
|
|
669
|
+
...prev.errorMap,
|
|
670
|
+
[errorMapKey]: fieldError,
|
|
671
|
+
},
|
|
672
|
+
}))
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this.state.errorMap[errorMapKey] !== formError) {
|
|
629
678
|
this.store.setState((prev) => ({
|
|
630
679
|
...prev,
|
|
631
680
|
errorMap: {
|
|
632
681
|
...prev.errorMap,
|
|
633
|
-
[errorMapKey]:
|
|
682
|
+
[errorMapKey]: formError,
|
|
634
683
|
},
|
|
635
684
|
}))
|
|
636
685
|
}
|
|
637
|
-
|
|
686
|
+
|
|
687
|
+
if (formError || fieldErrors) {
|
|
638
688
|
hasErrored = true
|
|
639
689
|
}
|
|
640
690
|
}
|
|
@@ -659,7 +709,7 @@ export class FormApi<
|
|
|
659
709
|
}))
|
|
660
710
|
}
|
|
661
711
|
|
|
662
|
-
return { hasErrored }
|
|
712
|
+
return { hasErrored, fieldsErrorMap }
|
|
663
713
|
}
|
|
664
714
|
|
|
665
715
|
/**
|
|
@@ -667,7 +717,7 @@ export class FormApi<
|
|
|
667
717
|
*/
|
|
668
718
|
validateAsync = async (
|
|
669
719
|
cause: ValidationCause,
|
|
670
|
-
): Promise<
|
|
720
|
+
): Promise<FieldsErrorMapFromValidator<TFormData>> => {
|
|
671
721
|
const validates = getAsyncValidatorArray(cause, this.options)
|
|
672
722
|
|
|
673
723
|
if (!this.state.isFormValidating) {
|
|
@@ -678,7 +728,11 @@ export class FormApi<
|
|
|
678
728
|
* We have to use a for loop and generate our promises this way, otherwise it won't be sync
|
|
679
729
|
* when there are no validators needed to be run
|
|
680
730
|
*/
|
|
681
|
-
const promises: Promise<
|
|
731
|
+
const promises: Promise<ValidationPromiseResult<TFormData>>[] = []
|
|
732
|
+
|
|
733
|
+
let fieldErrors:
|
|
734
|
+
| Partial<Record<DeepKeys<TFormData>, ValidationError>>
|
|
735
|
+
| undefined
|
|
682
736
|
|
|
683
737
|
for (const validateObj of validates) {
|
|
684
738
|
if (!validateObj.validate) continue
|
|
@@ -693,7 +747,7 @@ export class FormApi<
|
|
|
693
747
|
}
|
|
694
748
|
|
|
695
749
|
promises.push(
|
|
696
|
-
new Promise<
|
|
750
|
+
new Promise<ValidationPromiseResult<TFormData>>(async (resolve) => {
|
|
697
751
|
let rawError!: ValidationError | undefined
|
|
698
752
|
try {
|
|
699
753
|
rawError = await new Promise((rawResolve, rawReject) => {
|
|
@@ -719,23 +773,65 @@ export class FormApi<
|
|
|
719
773
|
} catch (e: unknown) {
|
|
720
774
|
rawError = e as ValidationError
|
|
721
775
|
}
|
|
722
|
-
const
|
|
776
|
+
const { formError, fieldErrors: fieldErrorsFromNormalizeError } =
|
|
777
|
+
normalizeError<TFormData>(rawError)
|
|
778
|
+
|
|
779
|
+
if (fieldErrorsFromNormalizeError) {
|
|
780
|
+
fieldErrors = fieldErrors
|
|
781
|
+
? { ...fieldErrors, ...fieldErrorsFromNormalizeError }
|
|
782
|
+
: fieldErrorsFromNormalizeError
|
|
783
|
+
}
|
|
784
|
+
const errorMapKey = getErrorMapKey(validateObj.cause)
|
|
785
|
+
|
|
786
|
+
if (fieldErrors) {
|
|
787
|
+
for (const [field, fieldError] of Object.entries(fieldErrors)) {
|
|
788
|
+
const fieldMeta = this.getFieldMeta(field as DeepKeys<TFormData>)
|
|
789
|
+
if (fieldMeta && fieldMeta.errorMap[errorMapKey] !== fieldError) {
|
|
790
|
+
this.setFieldMeta(field as DeepKeys<TFormData>, (prev) => ({
|
|
791
|
+
...prev,
|
|
792
|
+
errorMap: {
|
|
793
|
+
...prev.errorMap,
|
|
794
|
+
[errorMapKey]: fieldError,
|
|
795
|
+
},
|
|
796
|
+
}))
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
723
800
|
this.store.setState((prev) => ({
|
|
724
801
|
...prev,
|
|
725
802
|
errorMap: {
|
|
726
803
|
...prev.errorMap,
|
|
727
|
-
[
|
|
804
|
+
[errorMapKey]: formError,
|
|
728
805
|
},
|
|
729
806
|
}))
|
|
730
807
|
|
|
731
|
-
resolve(
|
|
808
|
+
resolve(fieldErrors ? { fieldErrors, errorMapKey } : undefined)
|
|
732
809
|
}),
|
|
733
810
|
)
|
|
734
811
|
}
|
|
735
812
|
|
|
736
|
-
let results:
|
|
813
|
+
let results: ValidationPromiseResult<TFormData>[] = []
|
|
814
|
+
|
|
815
|
+
const fieldsErrorMap: FieldsErrorMapFromValidator<TFormData> = {}
|
|
737
816
|
if (promises.length) {
|
|
738
817
|
results = await Promise.all(promises)
|
|
818
|
+
for (const fieldValidationResult of results) {
|
|
819
|
+
if (fieldValidationResult?.fieldErrors) {
|
|
820
|
+
const { errorMapKey } = fieldValidationResult
|
|
821
|
+
|
|
822
|
+
for (const [field, fieldError] of Object.entries(
|
|
823
|
+
fieldValidationResult.fieldErrors,
|
|
824
|
+
)) {
|
|
825
|
+
const oldErrorMap =
|
|
826
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] || {}
|
|
827
|
+
const newErrorMap = {
|
|
828
|
+
...oldErrorMap,
|
|
829
|
+
[errorMapKey]: fieldError,
|
|
830
|
+
}
|
|
831
|
+
fieldsErrorMap[field as DeepKeys<TFormData>] = newErrorMap
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
739
835
|
}
|
|
740
836
|
|
|
741
837
|
this.store.setState((prev) => ({
|
|
@@ -743,7 +839,7 @@ export class FormApi<
|
|
|
743
839
|
isFormValidating: false,
|
|
744
840
|
}))
|
|
745
841
|
|
|
746
|
-
return
|
|
842
|
+
return fieldsErrorMap
|
|
747
843
|
}
|
|
748
844
|
|
|
749
845
|
/**
|
|
@@ -751,12 +847,14 @@ export class FormApi<
|
|
|
751
847
|
*/
|
|
752
848
|
validate = (
|
|
753
849
|
cause: ValidationCause,
|
|
754
|
-
):
|
|
850
|
+
):
|
|
851
|
+
| FieldsErrorMapFromValidator<TFormData>
|
|
852
|
+
| Promise<FieldsErrorMapFromValidator<TFormData>> => {
|
|
755
853
|
// Attempt to sync validate first
|
|
756
|
-
const { hasErrored } = this.validateSync(cause)
|
|
854
|
+
const { hasErrored, fieldsErrorMap } = this.validateSync(cause)
|
|
757
855
|
|
|
758
856
|
if (hasErrored && !this.options.asyncAlways) {
|
|
759
|
-
return
|
|
857
|
+
return fieldsErrorMap
|
|
760
858
|
}
|
|
761
859
|
|
|
762
860
|
// No error? Attempt async validation
|
|
@@ -1109,16 +1207,25 @@ export class FormApi<
|
|
|
1109
1207
|
}
|
|
1110
1208
|
}
|
|
1111
1209
|
|
|
1112
|
-
function normalizeError(rawError?:
|
|
1210
|
+
function normalizeError<TFormData>(rawError?: FormValidationError<TFormData>): {
|
|
1211
|
+
formError: ValidationError
|
|
1212
|
+
fieldErrors?: Partial<Record<DeepKeys<TFormData>, ValidationError>>
|
|
1213
|
+
} {
|
|
1113
1214
|
if (rawError) {
|
|
1215
|
+
if (typeof rawError === 'object') {
|
|
1216
|
+
const formError = normalizeError(rawError.form).formError
|
|
1217
|
+
const fieldErrors = rawError.fields
|
|
1218
|
+
return { formError, fieldErrors }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1114
1221
|
if (typeof rawError !== 'string') {
|
|
1115
|
-
return 'Invalid Form Values'
|
|
1222
|
+
return { formError: 'Invalid Form Values' }
|
|
1116
1223
|
}
|
|
1117
1224
|
|
|
1118
|
-
return rawError
|
|
1225
|
+
return { formError: rawError }
|
|
1119
1226
|
}
|
|
1120
1227
|
|
|
1121
|
-
return undefined
|
|
1228
|
+
return { formError: undefined }
|
|
1122
1229
|
}
|
|
1123
1230
|
|
|
1124
1231
|
function getErrorMapKey(cause: ValidationCause) {
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type DeepKeys } from './util-types'
|
|
2
|
+
|
|
1
3
|
export type ValidationError = undefined | false | null | string
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -9,6 +11,14 @@ export type Validator<Type, Fn = unknown> = () => {
|
|
|
9
11
|
validateAsync(options: { value: Type }, fn: Fn): Promise<ValidationError>
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Parameters in common for all validator adapters, making it easier to swap adapter
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
export type ValidatorAdapterParams<TError = unknown> = {
|
|
19
|
+
transformErrors?: (errors: TError[]) => ValidationError
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* "server" is only intended for SSR/SSG validation and should not execute anything
|
|
14
24
|
* @private
|
|
@@ -27,6 +37,26 @@ export type ValidationErrorMap = {
|
|
|
27
37
|
[K in ValidationErrorMapKeys]?: ValidationError
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @private
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* {
|
|
46
|
+
* form: 'This form contains an error',
|
|
47
|
+
* fields: {
|
|
48
|
+
* age: "Must be 13 or older to register"
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* ````
|
|
52
|
+
*/
|
|
53
|
+
export type FormValidationError<TFormData> =
|
|
54
|
+
| ValidationError
|
|
55
|
+
| {
|
|
56
|
+
form?: ValidationError
|
|
57
|
+
fields: Partial<Record<DeepKeys<TFormData>, ValidationError>>
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
/**
|
|
31
61
|
* @private
|
|
32
62
|
*/
|