@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/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 `z.string().min(1)` if `zodAdapter` is passed
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 `z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' })` if `zodAdapter` is passed
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 when run when subscribing to blur event of input.
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 `z.string().min(1)` if `zodAdapter` is passed
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 `z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' })` if `zodAdapter` is passed
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 when run when subscribing to submit event of input.
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 `z.string().min(1)` if `zodAdapter` is passed
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 `z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' })` if `zodAdapter` is passed
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 = (cause: ValidationCause) => {
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)]: error,
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 (cause: ValidationCause) => {
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 key = getErrorMapKey(validateObj.cause)
839
- const fieldValidatorMeta = field.getInfo().validationMetaMap[key]
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[key] = {
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
- [getErrorMapKey(cause)]: error,
916
+ [errorMapKey]: fieldError,
884
917
  },
885
918
  }
886
919
  })
887
920
 
888
- resolve(error)
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 and validatePristine is false, do not validate
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
- * @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
  /**
@@ -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 = (cause: ValidationCause) => {
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 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
- )
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
- if (this.state.errorMap[errorMapKey] !== error) {
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]: error,
701
+ [errorMapKey]: formError,
634
702
  },
635
703
  }))
636
704
  }
637
- if (error) {
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<ValidationError[]> => {
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<ValidationError | undefined>[] = []
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<ValidationError | undefined>(async (resolve) => {
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 error = normalizeError(rawError)
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
- [getErrorMapKey(cause)]: error,
823
+ [errorMapKey]: formError,
728
824
  },
729
825
  }))
730
826
 
731
- resolve(error)
827
+ resolve(fieldErrors ? { fieldErrors, errorMapKey } : undefined)
732
828
  }),
733
829
  )
734
830
  }
735
831
 
736
- let results: ValidationError[] = []
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 results.filter(Boolean)
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
- ): ValidationError[] | Promise<ValidationError[]> => {
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 this.state.errors
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?: ValidationError) {
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) {