@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/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
- * @private
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
- }) => ValidationError
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
- }) => ValidationError | Promise<ValidationError>
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 ValidationError
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 return a `ValidationError` or a promise of `Promise<ValidationError>`
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 = (cause: ValidationCause) => {
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 error = normalizeError(
618
- this.runValidator({
619
- validate: validateObj.validate,
620
- value: {
621
- value: this.state.values,
622
- formApi: this,
623
- },
624
- type: 'validate',
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
- if (this.state.errorMap[errorMapKey] !== error) {
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]: error,
682
+ [errorMapKey]: formError,
634
683
  },
635
684
  }))
636
685
  }
637
- if (error) {
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<ValidationError[]> => {
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<ValidationError | undefined>[] = []
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<ValidationError | undefined>(async (resolve) => {
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 error = normalizeError(rawError)
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
- [getErrorMapKey(cause)]: error,
804
+ [errorMapKey]: formError,
728
805
  },
729
806
  }))
730
807
 
731
- resolve(error)
808
+ resolve(fieldErrors ? { fieldErrors, errorMapKey } : undefined)
732
809
  }),
733
810
  )
734
811
  }
735
812
 
736
- let results: ValidationError[] = []
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 results.filter(Boolean)
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
- ): ValidationError[] | Promise<ValidationError[]> => {
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 this.state.errors
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?: ValidationError) {
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
  */