@tanstack/react-form 0.13.6 → 0.14.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 (42) hide show
  1. package/dist/cjs/createFormFactory.cjs.map +1 -1
  2. package/dist/cjs/createFormFactory.d.cts +3 -3
  3. package/dist/cjs/types.d.cts +2 -2
  4. package/dist/cjs/useField.cjs +5 -28
  5. package/dist/cjs/useField.cjs.map +1 -1
  6. package/dist/cjs/useField.d.cts +4 -12
  7. package/dist/cjs/useForm.cjs +4 -6
  8. package/dist/cjs/useForm.cjs.map +1 -1
  9. package/dist/cjs/useForm.d.cts +14 -4
  10. package/dist/esm/createFormFactory.d.ts +3 -3
  11. package/dist/esm/createFormFactory.js.map +1 -1
  12. package/dist/esm/types.d.ts +2 -2
  13. package/dist/esm/useField.d.ts +4 -12
  14. package/dist/esm/useField.js +7 -30
  15. package/dist/esm/useField.js.map +1 -1
  16. package/dist/esm/useForm.d.ts +14 -4
  17. package/dist/esm/useForm.js +4 -6
  18. package/dist/esm/useForm.js.map +1 -1
  19. package/package.json +3 -3
  20. package/src/createFormFactory.ts +4 -4
  21. package/src/tests/createFormFactory.test.tsx +4 -5
  22. package/src/tests/useField.test-d.tsx +5 -5
  23. package/src/tests/useField.test.tsx +257 -32
  24. package/src/tests/useForm.test-d.tsx +1 -1
  25. package/src/tests/useForm.test.tsx +48 -25
  26. package/src/types.ts +8 -2
  27. package/src/useField.tsx +26 -68
  28. package/src/useForm.tsx +19 -14
  29. package/dist/cjs/formContext.cjs +0 -14
  30. package/dist/cjs/formContext.cjs.map +0 -1
  31. package/dist/cjs/formContext.d.cts +0 -10
  32. package/dist/cjs/useIsomorphicEffectOnce.cjs +0 -30
  33. package/dist/cjs/useIsomorphicEffectOnce.cjs.map +0 -1
  34. package/dist/cjs/useIsomorphicEffectOnce.d.cts +0 -5
  35. package/dist/esm/formContext.d.ts +0 -10
  36. package/dist/esm/formContext.js +0 -14
  37. package/dist/esm/formContext.js.map +0 -1
  38. package/dist/esm/useIsomorphicEffectOnce.d.ts +0 -5
  39. package/dist/esm/useIsomorphicEffectOnce.js +0 -30
  40. package/dist/esm/useIsomorphicEffectOnce.js.map +0 -1
  41. package/src/formContext.ts +0 -17
  42. package/src/useIsomorphicEffectOnce.ts +0 -39
@@ -1,5 +1,5 @@
1
- import { assertType } from 'vitest'
2
1
  import * as React from 'react'
2
+ import { assertType, it } from 'vitest'
3
3
  import { useForm } from '../useForm'
4
4
 
5
5
  it('should type state.value properly', () => {
@@ -12,7 +12,7 @@ it('should type state.value properly', () => {
12
12
  } as const)
13
13
 
14
14
  return (
15
- <form.Provider>
15
+ <>
16
16
  <form.Field
17
17
  name="firstName"
18
18
  children={(field) => {
@@ -25,7 +25,7 @@ it('should type state.value properly', () => {
25
25
  assertType<84>(field.state.value)
26
26
  }}
27
27
  />
28
- </form.Provider>
28
+ </>
29
29
  )
30
30
  }
31
31
  })
@@ -40,7 +40,7 @@ it('should type onChange properly', () => {
40
40
  } as const)
41
41
 
42
42
  return (
43
- <form.Provider>
43
+ <>
44
44
  <form.Field
45
45
  name="firstName"
46
46
  validators={{
@@ -61,7 +61,7 @@ it('should type onChange properly', () => {
61
61
  }}
62
62
  children={() => null}
63
63
  />
64
- </form.Provider>
64
+ </>
65
65
  )
66
66
  }
67
67
  })
@@ -1,11 +1,10 @@
1
- /// <reference lib="dom" />
2
1
  import * as React from 'react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
3
  import { render, waitFor } from '@testing-library/react'
4
4
  import userEvent from '@testing-library/user-event'
5
- import '@testing-library/jest-dom'
6
- import { createFormFactory } from '../index'
5
+ import { createFormFactory, useForm } from '../index'
7
6
  import { sleep } from './utils'
8
- import type { FormApi } from '../index'
7
+ import type { FieldApi, FormApi } from '../index'
9
8
 
10
9
  const user = userEvent.setup()
11
10
 
@@ -27,7 +26,7 @@ describe('useField', () => {
27
26
  })
28
27
 
29
28
  return (
30
- <form.Provider>
29
+ <>
31
30
  <form.Field
32
31
  name="firstName"
33
32
  children={(field) => {
@@ -41,7 +40,7 @@ describe('useField', () => {
41
40
  )
42
41
  }}
43
42
  />
44
- </form.Provider>
43
+ </>
45
44
  )
46
45
  }
47
46
 
@@ -67,7 +66,7 @@ describe('useField', () => {
67
66
  })
68
67
 
69
68
  return (
70
- <form.Provider>
69
+ <>
71
70
  <form.Field
72
71
  name="firstName"
73
72
  defaultValue="otherName"
@@ -82,7 +81,7 @@ describe('useField', () => {
82
81
  )
83
82
  }}
84
83
  />
85
- </form.Provider>
84
+ </>
86
85
  )
87
86
  }
88
87
 
@@ -101,10 +100,15 @@ describe('useField', () => {
101
100
  const formFactory = createFormFactory<Person>()
102
101
 
103
102
  function Comp() {
104
- const form = formFactory.useForm()
103
+ const form = formFactory.useForm({
104
+ defaultValues: {
105
+ firstName: '',
106
+ lastName: '',
107
+ },
108
+ })
105
109
 
106
110
  return (
107
- <form.Provider>
111
+ <>
108
112
  <form.Field
109
113
  name="firstName"
110
114
  validators={{
@@ -123,7 +127,7 @@ describe('useField', () => {
123
127
  </div>
124
128
  )}
125
129
  />
126
- </form.Provider>
130
+ </>
127
131
  )
128
132
  }
129
133
 
@@ -143,10 +147,15 @@ describe('useField', () => {
143
147
  const formFactory = createFormFactory<Person>()
144
148
 
145
149
  function Comp() {
146
- const form = formFactory.useForm()
150
+ const form = formFactory.useForm({
151
+ defaultValues: {
152
+ firstName: '',
153
+ lastName: '',
154
+ },
155
+ })
147
156
 
148
157
  return (
149
- <form.Provider>
158
+ <>
150
159
  <form.Field
151
160
  name="firstName"
152
161
  defaultMeta={{ isTouched: true }}
@@ -166,7 +175,7 @@ describe('useField', () => {
166
175
  </div>
167
176
  )}
168
177
  />
169
- </form.Provider>
178
+ </>
170
179
  )
171
180
  }
172
181
 
@@ -188,10 +197,15 @@ describe('useField', () => {
188
197
  const formFactory = createFormFactory<Person>()
189
198
 
190
199
  function Comp() {
191
- const form = formFactory.useForm()
200
+ const form = formFactory.useForm({
201
+ defaultValues: {
202
+ firstName: '',
203
+ lastName: '',
204
+ },
205
+ })
192
206
 
193
207
  return (
194
- <form.Provider>
208
+ <>
195
209
  <form.Field
196
210
  name="firstName"
197
211
  defaultMeta={{ isTouched: true }}
@@ -215,7 +229,7 @@ describe('useField', () => {
215
229
  </div>
216
230
  )}
217
231
  />
218
- </form.Provider>
232
+ </>
219
233
  )
220
234
  }
221
235
 
@@ -239,10 +253,15 @@ describe('useField', () => {
239
253
  const formFactory = createFormFactory<Person>()
240
254
 
241
255
  function Comp() {
242
- const form = formFactory.useForm()
256
+ const form = formFactory.useForm({
257
+ defaultValues: {
258
+ firstName: '',
259
+ lastName: '',
260
+ },
261
+ })
243
262
 
244
263
  return (
245
- <form.Provider>
264
+ <>
246
265
  <form.Field
247
266
  name="firstName"
248
267
  defaultMeta={{ isTouched: true }}
@@ -265,7 +284,7 @@ describe('useField', () => {
265
284
  </div>
266
285
  )}
267
286
  />
268
- </form.Provider>
287
+ </>
269
288
  )
270
289
  }
271
290
 
@@ -288,10 +307,15 @@ describe('useField', () => {
288
307
  const formFactory = createFormFactory<Person>()
289
308
 
290
309
  function Comp() {
291
- const form = formFactory.useForm()
310
+ const form = formFactory.useForm({
311
+ defaultValues: {
312
+ firstName: '',
313
+ lastName: '',
314
+ },
315
+ })
292
316
 
293
317
  return (
294
- <form.Provider>
318
+ <>
295
319
  <form.Field
296
320
  name="firstName"
297
321
  defaultMeta={{ isTouched: true }}
@@ -319,7 +343,7 @@ describe('useField', () => {
319
343
  </div>
320
344
  )}
321
345
  />
322
- </form.Provider>
346
+ </>
323
347
  )
324
348
  }
325
349
 
@@ -346,10 +370,15 @@ describe('useField', () => {
346
370
  const formFactory = createFormFactory<Person>()
347
371
 
348
372
  function Comp() {
349
- const form = formFactory.useForm()
373
+ const form = formFactory.useForm({
374
+ defaultValues: {
375
+ firstName: '',
376
+ lastName: '',
377
+ },
378
+ })
350
379
 
351
380
  return (
352
- <form.Provider>
381
+ <>
353
382
  <form.Field
354
383
  name="firstName"
355
384
  defaultMeta={{ isTouched: true }}
@@ -374,7 +403,7 @@ describe('useField', () => {
374
403
  </div>
375
404
  )}
376
405
  />
377
- </form.Provider>
406
+ </>
378
407
  )
379
408
  }
380
409
 
@@ -395,9 +424,14 @@ describe('useField', () => {
395
424
  const formFactory = createFormFactory<Person>()
396
425
  let form: FormApi<Person> | null = null
397
426
  function Comp() {
398
- form = formFactory.useForm()
427
+ form = formFactory.useForm({
428
+ defaultValues: {
429
+ firstName: '',
430
+ lastName: '',
431
+ },
432
+ })
399
433
  return (
400
- <form.Provider>
434
+ <>
401
435
  <form.Field
402
436
  name="firstName"
403
437
  defaultValue="hello"
@@ -413,7 +447,7 @@ describe('useField', () => {
413
447
  )
414
448
  }}
415
449
  />
416
- </form.Provider>
450
+ </>
417
451
  )
418
452
  }
419
453
 
@@ -433,9 +467,14 @@ describe('useField', () => {
433
467
  const formFactory = createFormFactory<Person>()
434
468
  let form: FormApi<Person> | null = null
435
469
  function Comp() {
436
- form = formFactory.useForm()
470
+ form = formFactory.useForm({
471
+ defaultValues: {
472
+ firstName: '',
473
+ lastName: '',
474
+ },
475
+ })
437
476
  return (
438
- <form.Provider>
477
+ <>
439
478
  <form.Field
440
479
  name="firstName"
441
480
  defaultValue="hello"
@@ -451,7 +490,7 @@ describe('useField', () => {
451
490
  )
452
491
  }}
453
492
  />
454
- </form.Provider>
493
+ </>
455
494
  )
456
495
  }
457
496
 
@@ -462,4 +501,190 @@ describe('useField', () => {
462
501
  const info = form!.fieldInfo
463
502
  expect(Object.keys(info)).toHaveLength(0)
464
503
  })
504
+
505
+ it('should handle strict mode properly with conditional fields', async () => {
506
+ function FieldInfo({ field }: { field: FieldApi<any, any> }) {
507
+ return (
508
+ <>
509
+ {field.state.meta.touchedErrors ? (
510
+ <em>{field.state.meta.touchedErrors}</em>
511
+ ) : null}
512
+ {field.state.meta.isValidating ? 'Validating...' : null}
513
+ </>
514
+ )
515
+ }
516
+
517
+ function Comp() {
518
+ const [showField, setShowField] = React.useState(true)
519
+
520
+ const form = useForm({
521
+ defaultValues: {
522
+ firstName: '',
523
+ lastName: '',
524
+ },
525
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
526
+ onSubmit: async () => {},
527
+ })
528
+
529
+ return (
530
+ <div>
531
+ <form
532
+ onSubmit={(e) => {
533
+ e.preventDefault()
534
+ e.stopPropagation()
535
+ void form.handleSubmit()
536
+ }}
537
+ >
538
+ <div>
539
+ {/* A type-safe field component*/}
540
+ {showField ? (
541
+ <form.Field
542
+ name="firstName"
543
+ validators={{
544
+ onChange: ({ value }) =>
545
+ !value ? 'A first name is required' : undefined,
546
+ }}
547
+ children={(field) => {
548
+ // Avoid hasty abstractions. Render props are great!
549
+ return (
550
+ <>
551
+ <label htmlFor={field.name}>First Name:</label>
552
+ <input
553
+ name={field.name}
554
+ value={field.state.value}
555
+ onBlur={field.handleBlur}
556
+ onChange={(e) => field.handleChange(e.target.value)}
557
+ />
558
+ <FieldInfo field={field} />
559
+ </>
560
+ )
561
+ }}
562
+ />
563
+ ) : null}
564
+ </div>
565
+ <div>
566
+ <form.Field
567
+ name="lastName"
568
+ children={(field) => (
569
+ <>
570
+ <label htmlFor={field.name}>Last Name:</label>
571
+ <input
572
+ name={field.name}
573
+ value={field.state.value}
574
+ onBlur={field.handleBlur}
575
+ onChange={(e) => field.handleChange(e.target.value)}
576
+ />
577
+ <FieldInfo field={field} />
578
+ </>
579
+ )}
580
+ />
581
+ </div>
582
+ <form.Subscribe
583
+ selector={(state) => [state.canSubmit, state.isSubmitting]}
584
+ children={([canSubmit, isSubmitting]) => (
585
+ <button type="submit" disabled={!canSubmit}>
586
+ {isSubmitting ? '...' : 'Submit'}
587
+ </button>
588
+ )}
589
+ />
590
+ <button type="button" onClick={() => setShowField((prev) => !prev)}>
591
+ {showField ? 'Hide field' : 'Show field'}
592
+ </button>
593
+ </form>
594
+ </div>
595
+ )
596
+ }
597
+
598
+ const { getByText, findByText, queryByText } = render(
599
+ <React.StrictMode>
600
+ <Comp />
601
+ </React.StrictMode>,
602
+ )
603
+
604
+ await user.click(getByText('Submit'))
605
+ expect(await findByText('A first name is required')).toBeInTheDocument()
606
+ await user.click(getByText('Hide field'))
607
+ await user.click(getByText('Submit'))
608
+ expect(queryByText('A first name is required')).not.toBeInTheDocument()
609
+ })
610
+
611
+ it('should handle arrays with subvalues', async () => {
612
+ const fn = vi.fn()
613
+ function Comp() {
614
+ const form = useForm({
615
+ defaultValues: {
616
+ people: [] as Array<{ age: number; name: string }>,
617
+ },
618
+ onSubmit: ({ value }) => fn(value),
619
+ })
620
+
621
+ return (
622
+ <div>
623
+ <form
624
+ onSubmit={(e) => {
625
+ e.preventDefault()
626
+ e.stopPropagation()
627
+ void form.handleSubmit()
628
+ }}
629
+ >
630
+ <form.Field name="people">
631
+ {(field) => {
632
+ return (
633
+ <div>
634
+ {field.state.value.map((_, i) => {
635
+ return (
636
+ <form.Field key={i} name={`people[${i}].name`}>
637
+ {(subField) => {
638
+ return (
639
+ <div>
640
+ <label>
641
+ <div>Name for person {i}</div>
642
+ <input
643
+ value={subField.state.value}
644
+ onChange={(e) =>
645
+ subField.handleChange(e.target.value)
646
+ }
647
+ />
648
+ </label>
649
+ </div>
650
+ )
651
+ }}
652
+ </form.Field>
653
+ )
654
+ })}
655
+ <button
656
+ onClick={() => field.pushValue({ name: '', age: 0 })}
657
+ type="button"
658
+ >
659
+ Add person
660
+ </button>
661
+ </div>
662
+ )
663
+ }}
664
+ </form.Field>
665
+ <form.Subscribe
666
+ selector={(state) => [state.canSubmit, state.isSubmitting]}
667
+ children={([canSubmit, isSubmitting]) => (
668
+ <button type="submit" disabled={!canSubmit}>
669
+ {isSubmitting ? '...' : 'Submit'}
670
+ </button>
671
+ )}
672
+ />
673
+ </form>
674
+ </div>
675
+ )
676
+ }
677
+
678
+ const { getByText, findByLabelText, queryByText, findByText } = render(
679
+ <Comp />,
680
+ )
681
+
682
+ expect(queryByText('Name for person 0')).not.toBeInTheDocument()
683
+ await user.click(getByText('Add person'))
684
+ const input = await findByLabelText('Name for person 0')
685
+ expect(input).toBeInTheDocument()
686
+ await user.type(input, 'John')
687
+ await user.click(await findByText('Submit'))
688
+ expect(fn).toHaveBeenCalledWith({ people: [{ name: 'John', age: 0 }] })
689
+ })
465
690
  })
@@ -1,5 +1,5 @@
1
- import { assertType } from 'vitest'
2
1
  import * as React from 'react'
2
+ import { assertType, it } from 'vitest'
3
3
  import { useForm } from '../useForm'
4
4
 
5
5
  it('should type onSubmit properly', () => {