@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.
Files changed (104) hide show
  1. package/dist/cjs/FieldApi.d.ts +95 -0
  2. package/dist/cjs/FormApi.d.ts +118 -0
  3. package/dist/cjs/index.cjs +926 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/cjs/index.d.cts +5 -0
  6. package/dist/cjs/index.d.ts +5 -0
  7. package/dist/cjs/index.js +926 -0
  8. package/dist/cjs/mergeForm.d.ts +4 -0
  9. package/dist/cjs/tests/FieldApi.spec.d.ts +1 -0
  10. package/dist/cjs/tests/FieldApi.test-d.d.ts +1 -0
  11. package/dist/cjs/tests/FormApi.spec.d.ts +1 -0
  12. package/dist/cjs/tests/mutateMergeDeep.spec.d.ts +1 -0
  13. package/dist/cjs/tests/utils.d.ts +1 -0
  14. package/dist/cjs/tests/utils.spec.d.ts +1 -0
  15. package/dist/cjs/types.d.ts +14 -0
  16. package/dist/cjs/utils.d.ts +57 -0
  17. package/dist/mjs/FieldApi.d.ts +95 -0
  18. package/dist/mjs/FormApi.d.ts +118 -0
  19. package/dist/mjs/index.d.mts +5 -0
  20. package/dist/mjs/index.d.ts +5 -0
  21. package/dist/mjs/index.js +926 -0
  22. package/dist/mjs/index.mjs +926 -0
  23. package/dist/mjs/index.mjs.map +1 -0
  24. package/dist/mjs/mergeForm.d.ts +4 -0
  25. package/dist/mjs/tests/FieldApi.spec.d.ts +1 -0
  26. package/dist/mjs/tests/FieldApi.test-d.d.ts +1 -0
  27. package/dist/mjs/tests/FormApi.spec.d.ts +1 -0
  28. package/dist/mjs/tests/mutateMergeDeep.spec.d.ts +1 -0
  29. package/dist/mjs/tests/utils.d.ts +1 -0
  30. package/dist/mjs/tests/utils.spec.d.ts +1 -0
  31. package/dist/mjs/types.d.ts +14 -0
  32. package/dist/mjs/utils.d.ts +57 -0
  33. package/package.json +16 -21
  34. package/src/FieldApi.ts +328 -236
  35. package/src/FormApi.ts +302 -216
  36. package/src/index.ts +1 -0
  37. package/src/mergeForm.ts +42 -0
  38. package/src/tests/FieldApi.spec.ts +135 -48
  39. package/src/tests/FieldApi.test-d.ts +10 -6
  40. package/src/tests/FormApi.spec.ts +171 -62
  41. package/src/tests/mutateMergeDeep.spec.ts +32 -0
  42. package/src/tests/utils.ts +1 -1
  43. package/src/types.ts +11 -2
  44. package/src/utils.ts +137 -14
  45. package/build/legacy/FieldApi.cjs +0 -340
  46. package/build/legacy/FieldApi.cjs.map +0 -1
  47. package/build/legacy/FieldApi.d.cts +0 -4
  48. package/build/legacy/FieldApi.d.ts +0 -4
  49. package/build/legacy/FieldApi.js +0 -315
  50. package/build/legacy/FieldApi.js.map +0 -1
  51. package/build/legacy/FormApi.cjs +0 -438
  52. package/build/legacy/FormApi.cjs.map +0 -1
  53. package/build/legacy/FormApi.d.cts +0 -4
  54. package/build/legacy/FormApi.d.ts +0 -4
  55. package/build/legacy/FormApi.js +0 -419
  56. package/build/legacy/FormApi.js.map +0 -1
  57. package/build/legacy/index.cjs +0 -31
  58. package/build/legacy/index.cjs.map +0 -1
  59. package/build/legacy/index.d.cts +0 -170
  60. package/build/legacy/index.d.ts +0 -170
  61. package/build/legacy/index.js +0 -6
  62. package/build/legacy/index.js.map +0 -1
  63. package/build/legacy/types.cjs +0 -19
  64. package/build/legacy/types.cjs.map +0 -1
  65. package/build/legacy/types.d.cts +0 -7
  66. package/build/legacy/types.d.ts +0 -7
  67. package/build/legacy/types.js +0 -1
  68. package/build/legacy/types.js.map +0 -1
  69. package/build/legacy/utils.cjs +0 -132
  70. package/build/legacy/utils.cjs.map +0 -1
  71. package/build/legacy/utils.d.cts +0 -37
  72. package/build/legacy/utils.d.ts +0 -37
  73. package/build/legacy/utils.js +0 -103
  74. package/build/legacy/utils.js.map +0 -1
  75. package/build/modern/FieldApi.cjs +0 -337
  76. package/build/modern/FieldApi.cjs.map +0 -1
  77. package/build/modern/FieldApi.d.cts +0 -4
  78. package/build/modern/FieldApi.d.ts +0 -4
  79. package/build/modern/FieldApi.js +0 -312
  80. package/build/modern/FieldApi.js.map +0 -1
  81. package/build/modern/FormApi.cjs +0 -431
  82. package/build/modern/FormApi.cjs.map +0 -1
  83. package/build/modern/FormApi.d.cts +0 -4
  84. package/build/modern/FormApi.d.ts +0 -4
  85. package/build/modern/FormApi.js +0 -412
  86. package/build/modern/FormApi.js.map +0 -1
  87. package/build/modern/index.cjs +0 -31
  88. package/build/modern/index.cjs.map +0 -1
  89. package/build/modern/index.d.cts +0 -170
  90. package/build/modern/index.d.ts +0 -170
  91. package/build/modern/index.js +0 -6
  92. package/build/modern/index.js.map +0 -1
  93. package/build/modern/types.cjs +0 -19
  94. package/build/modern/types.cjs.map +0 -1
  95. package/build/modern/types.d.cts +0 -7
  96. package/build/modern/types.d.ts +0 -7
  97. package/build/modern/types.js +0 -1
  98. package/build/modern/types.js.map +0 -1
  99. package/build/modern/utils.cjs +0 -132
  100. package/build/modern/utils.cjs.map +0 -1
  101. package/build/modern/utils.d.cts +0 -37
  102. package/build/modern/utils.d.ts +0 -37
  103. package/build/modern/utils.js +0 -103
  104. package/build/modern/utils.js.map +0 -1
@@ -24,7 +24,12 @@ describe('form api', () => {
24
24
  isValid: true,
25
25
  isValidating: false,
26
26
  submissionAttempts: 0,
27
- formValidationCount: 0,
27
+ validationMetaMap: {
28
+ onChange: undefined,
29
+ onBlur: undefined,
30
+ onSubmit: undefined,
31
+ onMount: undefined,
32
+ },
28
33
  })
29
34
  })
30
35
 
@@ -53,7 +58,12 @@ describe('form api', () => {
53
58
  isValid: true,
54
59
  isValidating: false,
55
60
  submissionAttempts: 0,
56
- formValidationCount: 0,
61
+ validationMetaMap: {
62
+ onChange: undefined,
63
+ onBlur: undefined,
64
+ onSubmit: undefined,
65
+ onMount: undefined,
66
+ },
57
67
  })
58
68
  })
59
69
 
@@ -80,7 +90,12 @@ describe('form api', () => {
80
90
  isValid: true,
81
91
  isValidating: false,
82
92
  submissionAttempts: 30,
83
- formValidationCount: 0,
93
+ validationMetaMap: {
94
+ onChange: undefined,
95
+ onBlur: undefined,
96
+ onSubmit: undefined,
97
+ onMount: undefined,
98
+ },
84
99
  })
85
100
  })
86
101
 
@@ -118,7 +133,13 @@ describe('form api', () => {
118
133
  isValid: true,
119
134
  isValidating: false,
120
135
  submissionAttempts: 300,
121
- formValidationCount: 0,
136
+ validationMetaMap: {
137
+ onChange: undefined,
138
+ onBlur: undefined,
139
+ onSubmit: undefined,
140
+ onMount: undefined,
141
+ onServer: undefined,
142
+ },
122
143
  })
123
144
  })
124
145
 
@@ -152,7 +173,12 @@ describe('form api', () => {
152
173
  isValid: true,
153
174
  isValidating: false,
154
175
  submissionAttempts: 0,
155
- formValidationCount: 0,
176
+ validationMetaMap: {
177
+ onChange: undefined,
178
+ onBlur: undefined,
179
+ onSubmit: undefined,
180
+ onMount: undefined,
181
+ },
156
182
  })
157
183
  })
158
184
 
@@ -234,7 +260,7 @@ describe('form api', () => {
234
260
  employees: Partial<Employee>[]
235
261
  }
236
262
 
237
- const form = new FormApi<Form, unknown>()
263
+ const form = new FormApi<Form>()
238
264
 
239
265
  const field = new FieldApi({
240
266
  form,
@@ -262,7 +288,7 @@ describe('form api', () => {
262
288
  employees: Partial<Employee>[]
263
289
  }
264
290
 
265
- const form = new FormApi<Form, unknown>()
291
+ const form = new FormApi<Form>()
266
292
 
267
293
  const field = new FieldApi({
268
294
  form,
@@ -362,7 +388,9 @@ describe('form api', () => {
362
388
  const field = new FieldApi({
363
389
  form,
364
390
  name: 'name',
365
- onChange: (v) => (v.length > 0 ? undefined : 'required'),
391
+ validators: {
392
+ onChange: ({ value }) => (value.length > 0 ? undefined : 'required'),
393
+ },
366
394
  })
367
395
 
368
396
  form.mount()
@@ -390,9 +418,11 @@ describe('form api', () => {
390
418
  defaultValues: {
391
419
  name: 'test',
392
420
  },
393
- onChange: (value) => {
394
- if (value.name === 'other') return 'Please enter a different value'
395
- return
421
+ validators: {
422
+ onChange: ({ value }) => {
423
+ if (value.name === 'other') return 'Please enter a different value'
424
+ return
425
+ },
396
426
  },
397
427
  })
398
428
 
@@ -418,10 +448,12 @@ describe('form api', () => {
418
448
  defaultValues: {
419
449
  name: 'test',
420
450
  },
421
- onChangeAsync: async (value) => {
422
- await sleep(1000)
423
- if (value.name === 'other') return 'Please enter a different value'
424
- return
451
+ validators: {
452
+ onChangeAsync: async ({ value }) => {
453
+ await sleep(1000)
454
+ if (value.name === 'other') return 'Please enter a different value'
455
+ return
456
+ },
425
457
  },
426
458
  })
427
459
  const field = new FieldApi({
@@ -449,11 +481,13 @@ describe('form api', () => {
449
481
  defaultValues: {
450
482
  name: 'test',
451
483
  },
452
- onChangeAsyncDebounceMs: 1000,
453
- onChangeAsync: async (value) => {
454
- await sleepMock(1000)
455
- if (value.name === 'other') return 'Please enter a different value'
456
- return
484
+ validators: {
485
+ onChangeAsyncDebounceMs: 1000,
486
+ onChangeAsync: async ({ value }) => {
487
+ await sleepMock(1000)
488
+ if (value.name === 'other') return 'Please enter a different value'
489
+ return
490
+ },
457
491
  },
458
492
  })
459
493
  const field = new FieldApi({
@@ -485,10 +519,12 @@ describe('form api', () => {
485
519
  name: 'test',
486
520
  },
487
521
  asyncDebounceMs: 1000,
488
- onChangeAsync: async (value) => {
489
- await sleepMock(1000)
490
- if (value.name === 'other') return 'Please enter a different value'
491
- return
522
+ validators: {
523
+ onChangeAsync: async ({ value }) => {
524
+ await sleepMock(1000)
525
+ if (value.name === 'other') return 'Please enter a different value'
526
+ return
527
+ },
492
528
  },
493
529
  })
494
530
  const field = new FieldApi({
@@ -516,9 +552,11 @@ describe('form api', () => {
516
552
  defaultValues: {
517
553
  name: 'other',
518
554
  },
519
- onBlur: (value) => {
520
- if (value.name === 'other') return 'Please enter a different value'
521
- return
555
+ validators: {
556
+ onBlur: ({ value }) => {
557
+ if (value.name === 'other') return 'Please enter a different value'
558
+ return
559
+ },
522
560
  },
523
561
  })
524
562
  const field = new FieldApi({
@@ -544,10 +582,12 @@ describe('form api', () => {
544
582
  defaultValues: {
545
583
  name: 'test',
546
584
  },
547
- onBlurAsync: async (value) => {
548
- await sleep(1000)
549
- if (value.name === 'other') return 'Please enter a different value'
550
- return
585
+ validators: {
586
+ onBlurAsync: async ({ value }) => {
587
+ await sleep(1000)
588
+ if (value.name === 'other') return 'Please enter a different value'
589
+ return
590
+ },
551
591
  },
552
592
  })
553
593
  const field = new FieldApi({
@@ -575,11 +615,13 @@ describe('form api', () => {
575
615
  defaultValues: {
576
616
  name: 'test',
577
617
  },
578
- onBlurAsyncDebounceMs: 1000,
579
- onBlurAsync: async (value) => {
580
- await sleepMock(10)
581
- if (value.name === 'other') return 'Please enter a different value'
582
- return
618
+ validators: {
619
+ onBlurAsyncDebounceMs: 1000,
620
+ onBlurAsync: async ({ value }) => {
621
+ await sleepMock(10)
622
+ if (value.name === 'other') return 'Please enter a different value'
623
+ return
624
+ },
583
625
  },
584
626
  })
585
627
  const field = new FieldApi({
@@ -612,10 +654,12 @@ describe('form api', () => {
612
654
  name: 'test',
613
655
  },
614
656
  asyncDebounceMs: 1000,
615
- onBlurAsync: async (value) => {
616
- await sleepMock(10)
617
- if (value.name === 'other') return 'Please enter a different value'
618
- return
657
+ validators: {
658
+ onBlurAsync: async ({ value }) => {
659
+ await sleepMock(10)
660
+ if (value.name === 'other') return 'Please enter a different value'
661
+ return
662
+ },
619
663
  },
620
664
  })
621
665
  const field = new FieldApi({
@@ -644,13 +688,15 @@ describe('form api', () => {
644
688
  defaultValues: {
645
689
  name: 'other',
646
690
  },
647
- onBlur: (value) => {
648
- if (value.name === 'other') return 'Please enter a different value'
649
- return
650
- },
651
- onChange: (value) => {
652
- if (value.name === 'other') return 'Please enter a different value'
653
- return
691
+ validators: {
692
+ onBlur: ({ value }) => {
693
+ if (value.name === 'other') return 'Please enter a different value'
694
+ return
695
+ },
696
+ onChange: ({ value }) => {
697
+ if (value.name === 'other') return 'Please enter a different value'
698
+ return
699
+ },
654
700
  },
655
701
  })
656
702
  const field = new FieldApi({
@@ -678,9 +724,11 @@ describe('form api', () => {
678
724
  defaultValues: {
679
725
  name: 'other',
680
726
  },
681
- onChange: (value) => {
682
- if (value.name === 'other') return 'Please enter a different value'
683
- return
727
+ validators: {
728
+ onChange: ({ value }) => {
729
+ if (value.name === 'other') return 'Please enter a different value'
730
+ return
731
+ },
684
732
  },
685
733
  })
686
734
  const field = new FieldApi({
@@ -706,9 +754,11 @@ describe('form api', () => {
706
754
  defaultValues: {
707
755
  name: 'other',
708
756
  },
709
- onMount: (value) => {
710
- if (value.name === 'other') return 'Please enter a different value'
711
- return
757
+ validators: {
758
+ onMount: ({ value }) => {
759
+ if (value.name === 'other') return 'Please enter a different value'
760
+ return
761
+ },
712
762
  },
713
763
  })
714
764
  const field = new FieldApi({
@@ -736,13 +786,19 @@ describe('form api', () => {
736
786
  const field = new FieldApi({
737
787
  form,
738
788
  name: 'firstName',
739
- onChange: (v) => (v.length > 0 ? undefined : 'first name is required'),
789
+ validators: {
790
+ onChange: ({ value }) =>
791
+ value.length > 0 ? undefined : 'first name is required',
792
+ },
740
793
  })
741
794
 
742
795
  const lastNameField = new FieldApi({
743
796
  form,
744
797
  name: 'lastName',
745
- onChange: (v) => (v.length > 0 ? undefined : 'last name is required'),
798
+ validators: {
799
+ onChange: ({ value }) =>
800
+ value.length > 0 ? undefined : 'last name is required',
801
+ },
746
802
  })
747
803
 
748
804
  field.mount()
@@ -770,11 +826,14 @@ describe('form api', () => {
770
826
  const field = new FieldApi({
771
827
  form,
772
828
  name: 'firstName',
773
- onChange: (v) => (v.length > 0 ? undefined : 'first name is required'),
774
- onBlur: (v) =>
775
- v.length > 3
776
- ? undefined
777
- : 'first name must be longer than 3 characters',
829
+ validators: {
830
+ onChange: ({ value }) =>
831
+ value.length > 0 ? undefined : 'first name is required',
832
+ onBlur: ({ value }) =>
833
+ value.length > 3
834
+ ? undefined
835
+ : 'first name must be longer than 3 characters',
836
+ },
778
837
  })
779
838
 
780
839
  field.mount()
@@ -798,7 +857,10 @@ describe('form api', () => {
798
857
  const field = new FieldApi({
799
858
  form,
800
859
  name: 'firstName',
801
- onSubmit: (v) => (v.length > 0 ? undefined : 'first name is required'),
860
+ validators: {
861
+ onSubmit: ({ value }) =>
862
+ value.length > 0 ? undefined : 'first name is required',
863
+ },
802
864
  })
803
865
 
804
866
  field.mount()
@@ -828,7 +890,10 @@ describe('form api', () => {
828
890
  const field = new FieldApi({
829
891
  form,
830
892
  name: 'firstName',
831
- onChange: (v) => (v.length > 0 ? undefined : 'first name is required'),
893
+ validators: {
894
+ onChange: ({ value }) =>
895
+ value.length > 0 ? undefined : 'first name is required',
896
+ },
832
897
  })
833
898
 
834
899
  field.mount()
@@ -839,4 +904,48 @@ describe('form api', () => {
839
904
  await form.validateAllFields('change')
840
905
  expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
841
906
  })
907
+
908
+ it('should show onSubmit errors', async () => {
909
+ const form = new FormApi({
910
+ defaultValues: {
911
+ firstName: '',
912
+ },
913
+ validators: {
914
+ onSubmit: ({ value }) =>
915
+ value.firstName.length > 0 ? undefined : 'first name is required',
916
+ },
917
+ })
918
+
919
+ const field = new FieldApi({
920
+ form,
921
+ name: 'firstName',
922
+ })
923
+
924
+ field.mount()
925
+
926
+ await form.handleSubmit()
927
+ expect(form.state.errors).toStrictEqual(['first name is required'])
928
+ })
929
+
930
+ it('should run onChange validation during submit', async () => {
931
+ const form = new FormApi({
932
+ defaultValues: {
933
+ firstName: '',
934
+ },
935
+ validators: {
936
+ onChange: ({ value }) =>
937
+ value.firstName.length > 0 ? undefined : 'first name is required',
938
+ },
939
+ })
940
+
941
+ const field = new FieldApi({
942
+ form,
943
+ name: 'firstName',
944
+ })
945
+
946
+ field.mount()
947
+
948
+ await form.handleSubmit()
949
+ expect(form.state.errors).toStrictEqual(['first name is required'])
950
+ })
842
951
  })
@@ -0,0 +1,32 @@
1
+ import { describe } from 'vitest'
2
+ import { mutateMergeDeep } from '../mergeForm'
3
+
4
+ describe('mutateMergeDeep', () => {
5
+ test('Should merge two objects by mutating', () => {
6
+ const a = { a: 1 }
7
+ const b = { b: 2 }
8
+ mutateMergeDeep(a, b)
9
+ expect(a).toStrictEqual({ a: 1, b: 2 })
10
+ })
11
+
12
+ test('Should merge two objects including overwriting with undefined', () => {
13
+ const a = { a: 1 }
14
+ const b = { a: undefined }
15
+ mutateMergeDeep(a, b)
16
+ expect(a).toStrictEqual({ a: undefined })
17
+ })
18
+
19
+ test('Should merge two object by merging arrays', () => {
20
+ const a = { a: [1] }
21
+ const b = { a: [2] }
22
+ mutateMergeDeep(a, b)
23
+ expect(a).toStrictEqual({ a: [1, 2] })
24
+ })
25
+
26
+ test('Should merge two deeply nested objects', () => {
27
+ const a = { a: { a: 1 } }
28
+ const b = { a: { b: 2 } }
29
+ mutateMergeDeep(a, b)
30
+ expect(a).toStrictEqual({ a: { a: 1, b: 2 } })
31
+ })
32
+ })
@@ -1,5 +1,5 @@
1
1
  export function sleep(timeout: number): Promise<void> {
2
- return new Promise((resolve, _reject) => {
2
+ return new Promise((resolve) => {
3
3
  setTimeout(resolve, timeout)
4
4
  })
5
5
  }
package/src/types.ts CHANGED
@@ -2,6 +2,15 @@ export type ValidationError = undefined | false | null | string
2
2
 
3
3
  // If/when TypeScript supports higher-kinded types, this should not be `unknown` anymore
4
4
  export type Validator<Type, Fn = unknown> = () => {
5
- validate(value: Type, fn: Fn): ValidationError
6
- validateAsync(value: Type, fn: Fn): Promise<ValidationError>
5
+ validate(options: { value: Type }, fn: Fn): ValidationError
6
+ validateAsync(options: { value: Type }, fn: Fn): Promise<ValidationError>
7
+ }
8
+
9
+ // "server" is only intended for SSR/SSG validation and should not execute anything
10
+ export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | 'server'
11
+
12
+ export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
13
+
14
+ export type ValidationErrorMap = {
15
+ [K in ValidationErrorMapKeys]?: ValidationError
7
16
  }
package/src/utils.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import type { ValidationCause, Validator } from './types'
2
+ import type { FormValidators } from './FormApi'
3
+ import type { FieldValidators } from './FieldApi'
4
+
1
5
  export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput
2
6
 
3
7
  export type Updater<TInput, TOutput = TInput> =
@@ -141,6 +145,125 @@ export function isNonEmptyArray(obj: any) {
141
145
  return !(Array.isArray(obj) && obj.length === 0)
142
146
  }
143
147
 
148
+ interface AsyncValidatorArrayPartialOptions<T> {
149
+ validators?: T
150
+ asyncDebounceMs?: number
151
+ }
152
+
153
+ interface AsyncValidator<T> {
154
+ cause: ValidationCause
155
+ validate: T
156
+ debounceMs: number
157
+ }
158
+
159
+ export function getAsyncValidatorArray<T>(
160
+ cause: ValidationCause,
161
+ options: AsyncValidatorArrayPartialOptions<T>,
162
+ ): T extends FieldValidators<any, any>
163
+ ? Array<
164
+ AsyncValidator<T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']>
165
+ >
166
+ : T extends FormValidators<any, any>
167
+ ? Array<
168
+ AsyncValidator<
169
+ T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']
170
+ >
171
+ >
172
+ : never {
173
+ const { asyncDebounceMs } = options
174
+ const {
175
+ onChangeAsync,
176
+ onBlurAsync,
177
+ onSubmitAsync,
178
+ onBlurAsyncDebounceMs,
179
+ onChangeAsyncDebounceMs,
180
+ onSubmitAsyncDebounceMs,
181
+ } = (options.validators || {}) as
182
+ | FieldValidators<any, any>
183
+ | FormValidators<any, any>
184
+
185
+ const defaultDebounceMs = asyncDebounceMs ?? 0
186
+
187
+ const changeValidator = {
188
+ cause: 'change',
189
+ validate: onChangeAsync,
190
+ debounceMs: onChangeAsyncDebounceMs ?? defaultDebounceMs,
191
+ } as const
192
+
193
+ const blurValidator = {
194
+ cause: 'blur',
195
+ validate: onBlurAsync,
196
+ debounceMs: onBlurAsyncDebounceMs ?? defaultDebounceMs,
197
+ } as const
198
+
199
+ const submitValidator = {
200
+ cause: 'submit',
201
+ validate: onSubmitAsync,
202
+ debounceMs: onSubmitAsyncDebounceMs ?? defaultDebounceMs,
203
+ } as const
204
+
205
+ switch (cause) {
206
+ case 'submit':
207
+ return [changeValidator, blurValidator, submitValidator] as never
208
+ case 'server':
209
+ return [] as never
210
+ case 'blur':
211
+ return [blurValidator] as never
212
+ case 'change':
213
+ default:
214
+ return [changeValidator] as never
215
+ }
216
+ }
217
+
218
+ interface SyncValidatorArrayPartialOptions<T> {
219
+ validators?: T
220
+ }
221
+
222
+ interface SyncValidator<T> {
223
+ cause: ValidationCause
224
+ validate: T
225
+ }
226
+
227
+ export function getSyncValidatorArray<T>(
228
+ cause: ValidationCause,
229
+ options: SyncValidatorArrayPartialOptions<T>,
230
+ ): T extends FieldValidators<any, any>
231
+ ? Array<SyncValidator<T['onChange'] | T['onBlur'] | T['onSubmit']>>
232
+ : T extends FormValidators<any, any>
233
+ ? Array<SyncValidator<T['onChange'] | T['onBlur'] | T['onSubmit']>>
234
+ : never {
235
+ const { onChange, onBlur, onSubmit } = (options.validators || {}) as
236
+ | FieldValidators<any, any>
237
+ | FormValidators<any, any>
238
+
239
+ const changeValidator = { cause: 'change', validate: onChange } as const
240
+ const blurValidator = { cause: 'blur', validate: onBlur } as const
241
+ const submitValidator = { cause: 'submit', validate: onSubmit } as const
242
+
243
+ // Allows us to clear onServer errors
244
+ const serverValidator = {
245
+ cause: 'server',
246
+ validate: () => undefined,
247
+ } as const
248
+
249
+ switch (cause) {
250
+ case 'submit':
251
+ return [
252
+ changeValidator,
253
+ blurValidator,
254
+ submitValidator,
255
+ serverValidator,
256
+ ] as never
257
+ case 'server':
258
+ return [serverValidator] as never
259
+ case 'blur':
260
+ return [blurValidator, serverValidator] as never
261
+ case 'change':
262
+ default:
263
+ return [changeValidator, serverValidator] as never
264
+ }
265
+ }
266
+
144
267
  export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
145
268
  Required<Pick<T, K>>
146
269
 
@@ -166,24 +289,24 @@ type AllowedIndexes<
166
289
  > = Tuple extends readonly []
167
290
  ? Keys
168
291
  : Tuple extends readonly [infer _, ...infer Tail]
169
- ? AllowedIndexes<Tail, Keys | Tail['length']>
170
- : Keys
292
+ ? AllowedIndexes<Tail, Keys | Tail['length']>
293
+ : Keys
171
294
 
172
295
  export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5
173
296
  ? never
174
297
  : unknown extends T
175
- ? string
176
- : object extends T
177
- ? string
178
- : T extends readonly any[] & IsTuple<T>
179
- ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>
180
- : T extends any[]
181
- ? DeepKeys<T[number], [...TDepth, any]>
182
- : T extends Date
183
- ? never
184
- : T extends object
185
- ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>
186
- : never
298
+ ? string
299
+ : object extends T
300
+ ? string
301
+ : T extends readonly any[] & IsTuple<T>
302
+ ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>
303
+ : T extends any[]
304
+ ? DeepKeys<T[number], [...TDepth, any]>
305
+ : T extends Date
306
+ ? never
307
+ : T extends object
308
+ ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>
309
+ : never
187
310
 
188
311
  type DeepKeysPrefix<
189
312
  T,