@tanstack/form-core 0.10.2 → 0.11.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 (55) hide show
  1. package/build/legacy/FieldApi.cjs +98 -125
  2. package/build/legacy/FieldApi.cjs.map +1 -1
  3. package/build/legacy/FieldApi.d.cts +1 -2
  4. package/build/legacy/FieldApi.d.ts +1 -2
  5. package/build/legacy/FieldApi.js +98 -125
  6. package/build/legacy/FieldApi.js.map +1 -1
  7. package/build/legacy/FormApi.cjs +128 -121
  8. package/build/legacy/FormApi.cjs.map +1 -1
  9. package/build/legacy/FormApi.d.cts +1 -2
  10. package/build/legacy/FormApi.d.ts +1 -2
  11. package/build/legacy/FormApi.js +130 -121
  12. package/build/legacy/FormApi.js.map +1 -1
  13. package/build/legacy/index.d.cts +163 -74
  14. package/build/legacy/index.d.ts +163 -74
  15. package/build/legacy/types.cjs.map +1 -1
  16. package/build/legacy/types.d.cts +12 -3
  17. package/build/legacy/types.d.ts +12 -3
  18. package/build/legacy/utils.cjs +55 -0
  19. package/build/legacy/utils.cjs.map +1 -1
  20. package/build/legacy/utils.d.cts +3 -37
  21. package/build/legacy/utils.d.ts +3 -37
  22. package/build/legacy/utils.js +53 -0
  23. package/build/legacy/utils.js.map +1 -1
  24. package/build/modern/FieldApi.cjs +98 -123
  25. package/build/modern/FieldApi.cjs.map +1 -1
  26. package/build/modern/FieldApi.d.cts +1 -2
  27. package/build/modern/FieldApi.d.ts +1 -2
  28. package/build/modern/FieldApi.js +98 -123
  29. package/build/modern/FieldApi.js.map +1 -1
  30. package/build/modern/FormApi.cjs +128 -120
  31. package/build/modern/FormApi.cjs.map +1 -1
  32. package/build/modern/FormApi.d.cts +1 -2
  33. package/build/modern/FormApi.d.ts +1 -2
  34. package/build/modern/FormApi.js +130 -120
  35. package/build/modern/FormApi.js.map +1 -1
  36. package/build/modern/index.d.cts +163 -74
  37. package/build/modern/index.d.ts +163 -74
  38. package/build/modern/types.cjs.map +1 -1
  39. package/build/modern/types.d.cts +12 -3
  40. package/build/modern/types.d.ts +12 -3
  41. package/build/modern/utils.cjs +55 -0
  42. package/build/modern/utils.cjs.map +1 -1
  43. package/build/modern/utils.d.cts +3 -37
  44. package/build/modern/utils.d.ts +3 -37
  45. package/build/modern/utils.js +53 -0
  46. package/build/modern/utils.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/FieldApi.ts +315 -241
  49. package/src/FormApi.ts +263 -213
  50. package/src/tests/FieldApi.spec.ts +135 -48
  51. package/src/tests/FieldApi.test-d.ts +10 -6
  52. package/src/tests/FormApi.spec.ts +192 -61
  53. package/src/tests/utils.ts +1 -1
  54. package/src/types.ts +10 -2
  55. package/src/utils.ts +106 -0
@@ -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,12 @@ 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
+ },
122
142
  })
123
143
  })
124
144
 
@@ -152,7 +172,12 @@ describe('form api', () => {
152
172
  isValid: true,
153
173
  isValidating: false,
154
174
  submissionAttempts: 0,
155
- formValidationCount: 0,
175
+ validationMetaMap: {
176
+ onChange: undefined,
177
+ onBlur: undefined,
178
+ onSubmit: undefined,
179
+ onMount: undefined,
180
+ },
156
181
  })
157
182
  })
158
183
 
@@ -234,7 +259,7 @@ describe('form api', () => {
234
259
  employees: Partial<Employee>[]
235
260
  }
236
261
 
237
- const form = new FormApi<Form, unknown>()
262
+ const form = new FormApi<Form>()
238
263
 
239
264
  const field = new FieldApi({
240
265
  form,
@@ -262,7 +287,7 @@ describe('form api', () => {
262
287
  employees: Partial<Employee>[]
263
288
  }
264
289
 
265
- const form = new FormApi<Form, unknown>()
290
+ const form = new FormApi<Form>()
266
291
 
267
292
  const field = new FieldApi({
268
293
  form,
@@ -362,7 +387,9 @@ describe('form api', () => {
362
387
  const field = new FieldApi({
363
388
  form,
364
389
  name: 'name',
365
- onChange: (v) => (v.length > 0 ? undefined : 'required'),
390
+ validators: {
391
+ onChange: ({ value }) => (value.length > 0 ? undefined : 'required'),
392
+ },
366
393
  })
367
394
 
368
395
  form.mount()
@@ -390,9 +417,11 @@ describe('form api', () => {
390
417
  defaultValues: {
391
418
  name: 'test',
392
419
  },
393
- onChange: (value) => {
394
- if (value.name === 'other') return 'Please enter a different value'
395
- return
420
+ validators: {
421
+ onChange: ({ value }) => {
422
+ if (value.name === 'other') return 'Please enter a different value'
423
+ return
424
+ },
396
425
  },
397
426
  })
398
427
 
@@ -418,10 +447,12 @@ describe('form api', () => {
418
447
  defaultValues: {
419
448
  name: 'test',
420
449
  },
421
- onChangeAsync: async (value) => {
422
- await sleep(1000)
423
- if (value.name === 'other') return 'Please enter a different value'
424
- return
450
+ validators: {
451
+ onChangeAsync: async ({ value }) => {
452
+ await sleep(1000)
453
+ if (value.name === 'other') return 'Please enter a different value'
454
+ return
455
+ },
425
456
  },
426
457
  })
427
458
  const field = new FieldApi({
@@ -449,11 +480,13 @@ describe('form api', () => {
449
480
  defaultValues: {
450
481
  name: 'test',
451
482
  },
452
- onChangeAsyncDebounceMs: 1000,
453
- onChangeAsync: async (value) => {
454
- await sleepMock(1000)
455
- if (value.name === 'other') return 'Please enter a different value'
456
- return
483
+ validators: {
484
+ onChangeAsyncDebounceMs: 1000,
485
+ onChangeAsync: async ({ value }) => {
486
+ await sleepMock(1000)
487
+ if (value.name === 'other') return 'Please enter a different value'
488
+ return
489
+ },
457
490
  },
458
491
  })
459
492
  const field = new FieldApi({
@@ -485,10 +518,12 @@ describe('form api', () => {
485
518
  name: 'test',
486
519
  },
487
520
  asyncDebounceMs: 1000,
488
- onChangeAsync: async (value) => {
489
- await sleepMock(1000)
490
- if (value.name === 'other') return 'Please enter a different value'
491
- return
521
+ validators: {
522
+ onChangeAsync: async ({ value }) => {
523
+ await sleepMock(1000)
524
+ if (value.name === 'other') return 'Please enter a different value'
525
+ return
526
+ },
492
527
  },
493
528
  })
494
529
  const field = new FieldApi({
@@ -516,9 +551,11 @@ describe('form api', () => {
516
551
  defaultValues: {
517
552
  name: 'other',
518
553
  },
519
- onBlur: (value) => {
520
- if (value.name === 'other') return 'Please enter a different value'
521
- return
554
+ validators: {
555
+ onBlur: ({ value }) => {
556
+ if (value.name === 'other') return 'Please enter a different value'
557
+ return
558
+ },
522
559
  },
523
560
  })
524
561
  const field = new FieldApi({
@@ -544,10 +581,12 @@ describe('form api', () => {
544
581
  defaultValues: {
545
582
  name: 'test',
546
583
  },
547
- onBlurAsync: async (value) => {
548
- await sleep(1000)
549
- if (value.name === 'other') return 'Please enter a different value'
550
- return
584
+ validators: {
585
+ onBlurAsync: async ({ value }) => {
586
+ await sleep(1000)
587
+ if (value.name === 'other') return 'Please enter a different value'
588
+ return
589
+ },
551
590
  },
552
591
  })
553
592
  const field = new FieldApi({
@@ -575,11 +614,13 @@ describe('form api', () => {
575
614
  defaultValues: {
576
615
  name: 'test',
577
616
  },
578
- onBlurAsyncDebounceMs: 1000,
579
- onBlurAsync: async (value) => {
580
- await sleepMock(10)
581
- if (value.name === 'other') return 'Please enter a different value'
582
- return
617
+ validators: {
618
+ onBlurAsyncDebounceMs: 1000,
619
+ onBlurAsync: async ({ value }) => {
620
+ await sleepMock(10)
621
+ if (value.name === 'other') return 'Please enter a different value'
622
+ return
623
+ },
583
624
  },
584
625
  })
585
626
  const field = new FieldApi({
@@ -612,10 +653,12 @@ describe('form api', () => {
612
653
  name: 'test',
613
654
  },
614
655
  asyncDebounceMs: 1000,
615
- onBlurAsync: async (value) => {
616
- await sleepMock(10)
617
- if (value.name === 'other') return 'Please enter a different value'
618
- return
656
+ validators: {
657
+ onBlurAsync: async ({ value }) => {
658
+ await sleepMock(10)
659
+ if (value.name === 'other') return 'Please enter a different value'
660
+ return
661
+ },
619
662
  },
620
663
  })
621
664
  const field = new FieldApi({
@@ -644,13 +687,15 @@ describe('form api', () => {
644
687
  defaultValues: {
645
688
  name: 'other',
646
689
  },
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
690
+ validators: {
691
+ onBlur: ({ value }) => {
692
+ if (value.name === 'other') return 'Please enter a different value'
693
+ return
694
+ },
695
+ onChange: ({ value }) => {
696
+ if (value.name === 'other') return 'Please enter a different value'
697
+ return
698
+ },
654
699
  },
655
700
  })
656
701
  const field = new FieldApi({
@@ -678,9 +723,11 @@ describe('form api', () => {
678
723
  defaultValues: {
679
724
  name: 'other',
680
725
  },
681
- onChange: (value) => {
682
- if (value.name === 'other') return 'Please enter a different value'
683
- return
726
+ validators: {
727
+ onChange: ({ value }) => {
728
+ if (value.name === 'other') return 'Please enter a different value'
729
+ return
730
+ },
684
731
  },
685
732
  })
686
733
  const field = new FieldApi({
@@ -706,9 +753,11 @@ describe('form api', () => {
706
753
  defaultValues: {
707
754
  name: 'other',
708
755
  },
709
- onMount: (value) => {
710
- if (value.name === 'other') return 'Please enter a different value'
711
- return
756
+ validators: {
757
+ onMount: ({ value }) => {
758
+ if (value.name === 'other') return 'Please enter a different value'
759
+ return
760
+ },
712
761
  },
713
762
  })
714
763
  const field = new FieldApi({
@@ -736,13 +785,19 @@ describe('form api', () => {
736
785
  const field = new FieldApi({
737
786
  form,
738
787
  name: 'firstName',
739
- onChange: (v) => (v.length > 0 ? undefined : 'first name is required'),
788
+ validators: {
789
+ onChange: ({ value }) =>
790
+ value.length > 0 ? undefined : 'first name is required',
791
+ },
740
792
  })
741
793
 
742
794
  const lastNameField = new FieldApi({
743
795
  form,
744
796
  name: 'lastName',
745
- onChange: (v) => (v.length > 0 ? undefined : 'last name is required'),
797
+ validators: {
798
+ onChange: ({ value }) =>
799
+ value.length > 0 ? undefined : 'last name is required',
800
+ },
746
801
  })
747
802
 
748
803
  field.mount()
@@ -770,11 +825,14 @@ describe('form api', () => {
770
825
  const field = new FieldApi({
771
826
  form,
772
827
  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',
828
+ validators: {
829
+ onChange: ({ value }) =>
830
+ value.length > 0 ? undefined : 'first name is required',
831
+ onBlur: ({ value }) =>
832
+ value.length > 3
833
+ ? undefined
834
+ : 'first name must be longer than 3 characters',
835
+ },
778
836
  })
779
837
 
780
838
  field.mount()
@@ -798,7 +856,10 @@ describe('form api', () => {
798
856
  const field = new FieldApi({
799
857
  form,
800
858
  name: 'firstName',
801
- onSubmit: (v) => (v.length > 0 ? undefined : 'first name is required'),
859
+ validators: {
860
+ onSubmit: ({ value }) =>
861
+ value.length > 0 ? undefined : 'first name is required',
862
+ },
802
863
  })
803
864
 
804
865
  field.mount()
@@ -816,4 +877,74 @@ describe('form api', () => {
816
877
  form.state.fieldMeta['firstName'].errorMap['onSubmit'],
817
878
  ).toBeUndefined()
818
879
  })
880
+
881
+ it('should validate all fields consistently', async () => {
882
+ const form = new FormApi({
883
+ defaultValues: {
884
+ firstName: '',
885
+ lastName: '',
886
+ },
887
+ })
888
+
889
+ const field = new FieldApi({
890
+ form,
891
+ name: 'firstName',
892
+ validators: {
893
+ onChange: ({ value }) =>
894
+ value.length > 0 ? undefined : 'first name is required',
895
+ },
896
+ })
897
+
898
+ field.mount()
899
+ form.mount()
900
+
901
+ await form.validateAllFields('change')
902
+ expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
903
+ await form.validateAllFields('change')
904
+ expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
905
+ })
906
+
907
+ it('should show onSubmit errors', async () => {
908
+ const form = new FormApi({
909
+ defaultValues: {
910
+ firstName: '',
911
+ },
912
+ validators: {
913
+ onSubmit: ({ value }) =>
914
+ value.firstName.length > 0 ? undefined : 'first name is required',
915
+ },
916
+ })
917
+
918
+ const field = new FieldApi({
919
+ form,
920
+ name: 'firstName',
921
+ })
922
+
923
+ field.mount()
924
+
925
+ await form.handleSubmit()
926
+ expect(form.state.errors).toStrictEqual(['first name is required'])
927
+ })
928
+
929
+ it('should run onChange validation during submit', async () => {
930
+ const form = new FormApi({
931
+ defaultValues: {
932
+ firstName: '',
933
+ },
934
+ validators: {
935
+ onChange: ({ value }) =>
936
+ value.firstName.length > 0 ? undefined : 'first name is required',
937
+ },
938
+ })
939
+
940
+ const field = new FieldApi({
941
+ form,
942
+ name: 'firstName',
943
+ })
944
+
945
+ field.mount()
946
+
947
+ await form.handleSubmit()
948
+ expect(form.state.errors).toStrictEqual(['first name is required'])
949
+ })
819
950
  })
@@ -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,14 @@ 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
+ export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
10
+
11
+ export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
12
+
13
+ export type ValidationErrorMap = {
14
+ [K in ValidationErrorMapKeys]?: ValidationError
7
15
  }
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,108 @@ 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<T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']>
169
+ >
170
+ : never {
171
+ const { asyncDebounceMs } = options
172
+ const {
173
+ onChangeAsync,
174
+ onBlurAsync,
175
+ onSubmitAsync,
176
+ onBlurAsyncDebounceMs,
177
+ onChangeAsyncDebounceMs,
178
+ onSubmitAsyncDebounceMs,
179
+ } = (options.validators || {}) as
180
+ | FieldValidators<any, any>
181
+ | FormValidators<any, any>
182
+
183
+ const defaultDebounceMs = asyncDebounceMs ?? 0
184
+
185
+ const changeValidator = {
186
+ cause: 'change',
187
+ validate: onChangeAsync,
188
+ debounceMs: onChangeAsyncDebounceMs ?? defaultDebounceMs,
189
+ } as const
190
+
191
+ const blurValidator = {
192
+ cause: 'blur',
193
+ validate: onBlurAsync,
194
+ debounceMs: onBlurAsyncDebounceMs ?? defaultDebounceMs,
195
+ } as const
196
+
197
+ const submitValidator = {
198
+ cause: 'submit',
199
+ validate: onSubmitAsync,
200
+ debounceMs: onSubmitAsyncDebounceMs ?? defaultDebounceMs,
201
+ } as const
202
+
203
+ switch (cause) {
204
+ case 'submit':
205
+ return [changeValidator, blurValidator, submitValidator] as never
206
+ case 'blur':
207
+ return [blurValidator] as never
208
+ case 'change':
209
+ default:
210
+ return [changeValidator] as never
211
+ }
212
+ }
213
+
214
+ interface SyncValidatorArrayPartialOptions<T> {
215
+ validators?: T
216
+ }
217
+
218
+ interface SyncValidator<T> {
219
+ cause: ValidationCause
220
+ validate: T
221
+ }
222
+
223
+ export function getSyncValidatorArray<T>(
224
+ cause: ValidationCause,
225
+ options: SyncValidatorArrayPartialOptions<T>,
226
+ ): T extends FieldValidators<any, any>
227
+ ? Array<SyncValidator<T['onChange'] | T['onBlur'] | T['onSubmit']>>
228
+ : T extends FormValidators<any, any>
229
+ ? Array<SyncValidator<T['onChange'] | T['onBlur'] | T['onSubmit']>>
230
+ : never {
231
+ const { onChange, onBlur, onSubmit } = (options.validators || {}) as
232
+ | FieldValidators<any, any>
233
+ | FormValidators<any, any>
234
+
235
+ const changeValidator = { cause: 'change', validate: onChange } as const
236
+ const blurValidator = { cause: 'blur', validate: onBlur } as const
237
+ const submitValidator = { cause: 'submit', validate: onSubmit } as const
238
+
239
+ switch (cause) {
240
+ case 'submit':
241
+ return [changeValidator, blurValidator, submitValidator] as never
242
+ case 'blur':
243
+ return [blurValidator] as never
244
+ case 'change':
245
+ default:
246
+ return [changeValidator] as never
247
+ }
248
+ }
249
+
144
250
  export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
145
251
  Required<Pick<T, K>>
146
252