@tanstack/react-form 0.13.7 → 0.16.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.
@@ -1,8 +1,7 @@
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
5
  import { createFormFactory, useForm } from '../index'
7
6
  import { sleep } from './utils'
8
7
  import type { FieldApi, FormApi } from '../index'
@@ -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
 
@@ -109,7 +108,7 @@ describe('useField', () => {
109
108
  })
110
109
 
111
110
  return (
112
- <form.Provider>
111
+ <>
113
112
  <form.Field
114
113
  name="firstName"
115
114
  validators={{
@@ -128,7 +127,7 @@ describe('useField', () => {
128
127
  </div>
129
128
  )}
130
129
  />
131
- </form.Provider>
130
+ </>
132
131
  )
133
132
  }
134
133
 
@@ -156,7 +155,7 @@ describe('useField', () => {
156
155
  })
157
156
 
158
157
  return (
159
- <form.Provider>
158
+ <>
160
159
  <form.Field
161
160
  name="firstName"
162
161
  defaultMeta={{ isTouched: true }}
@@ -176,7 +175,7 @@ describe('useField', () => {
176
175
  </div>
177
176
  )}
178
177
  />
179
- </form.Provider>
178
+ </>
180
179
  )
181
180
  }
182
181
 
@@ -206,7 +205,7 @@ describe('useField', () => {
206
205
  })
207
206
 
208
207
  return (
209
- <form.Provider>
208
+ <>
210
209
  <form.Field
211
210
  name="firstName"
212
211
  defaultMeta={{ isTouched: true }}
@@ -230,7 +229,7 @@ describe('useField', () => {
230
229
  </div>
231
230
  )}
232
231
  />
233
- </form.Provider>
232
+ </>
234
233
  )
235
234
  }
236
235
 
@@ -262,7 +261,7 @@ describe('useField', () => {
262
261
  })
263
262
 
264
263
  return (
265
- <form.Provider>
264
+ <>
266
265
  <form.Field
267
266
  name="firstName"
268
267
  defaultMeta={{ isTouched: true }}
@@ -285,7 +284,7 @@ describe('useField', () => {
285
284
  </div>
286
285
  )}
287
286
  />
288
- </form.Provider>
287
+ </>
289
288
  )
290
289
  }
291
290
 
@@ -316,7 +315,7 @@ describe('useField', () => {
316
315
  })
317
316
 
318
317
  return (
319
- <form.Provider>
318
+ <>
320
319
  <form.Field
321
320
  name="firstName"
322
321
  defaultMeta={{ isTouched: true }}
@@ -344,7 +343,7 @@ describe('useField', () => {
344
343
  </div>
345
344
  )}
346
345
  />
347
- </form.Provider>
346
+ </>
348
347
  )
349
348
  }
350
349
 
@@ -379,7 +378,7 @@ describe('useField', () => {
379
378
  })
380
379
 
381
380
  return (
382
- <form.Provider>
381
+ <>
383
382
  <form.Field
384
383
  name="firstName"
385
384
  defaultMeta={{ isTouched: true }}
@@ -404,7 +403,7 @@ describe('useField', () => {
404
403
  </div>
405
404
  )}
406
405
  />
407
- </form.Provider>
406
+ </>
408
407
  )
409
408
  }
410
409
 
@@ -432,7 +431,7 @@ describe('useField', () => {
432
431
  },
433
432
  })
434
433
  return (
435
- <form.Provider>
434
+ <>
436
435
  <form.Field
437
436
  name="firstName"
438
437
  defaultValue="hello"
@@ -448,7 +447,7 @@ describe('useField', () => {
448
447
  )
449
448
  }}
450
449
  />
451
- </form.Provider>
450
+ </>
452
451
  )
453
452
  }
454
453
 
@@ -475,7 +474,7 @@ describe('useField', () => {
475
474
  },
476
475
  })
477
476
  return (
478
- <form.Provider>
477
+ <>
479
478
  <form.Field
480
479
  name="firstName"
481
480
  defaultValue="hello"
@@ -491,7 +490,7 @@ describe('useField', () => {
491
490
  )
492
491
  }}
493
492
  />
494
- </form.Provider>
493
+ </>
495
494
  )
496
495
  }
497
496
 
@@ -529,74 +528,69 @@ describe('useField', () => {
529
528
 
530
529
  return (
531
530
  <div>
532
- <form.Provider>
533
- <form
534
- onSubmit={(e) => {
535
- e.preventDefault()
536
- e.stopPropagation()
537
- void form.handleSubmit()
538
- }}
539
- >
540
- <div>
541
- {/* A type-safe field component*/}
542
- {showField ? (
543
- <form.Field
544
- name="firstName"
545
- validators={{
546
- onChange: ({ value }) =>
547
- !value ? 'A first name is required' : undefined,
548
- }}
549
- children={(field) => {
550
- // Avoid hasty abstractions. Render props are great!
551
- return (
552
- <>
553
- <label htmlFor={field.name}>First Name:</label>
554
- <input
555
- name={field.name}
556
- value={field.state.value}
557
- onBlur={field.handleBlur}
558
- onChange={(e) => field.handleChange(e.target.value)}
559
- />
560
- <FieldInfo field={field} />
561
- </>
562
- )
563
- }}
564
- />
565
- ) : null}
566
- </div>
567
- <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 ? (
568
541
  <form.Field
569
- name="lastName"
570
- children={(field) => (
571
- <>
572
- <label htmlFor={field.name}>Last Name:</label>
573
- <input
574
- name={field.name}
575
- value={field.state.value}
576
- onBlur={field.handleBlur}
577
- onChange={(e) => field.handleChange(e.target.value)}
578
- />
579
- <FieldInfo field={field} />
580
- </>
581
- )}
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
+ }}
582
562
  />
583
- </div>
584
- <form.Subscribe
585
- selector={(state) => [state.canSubmit, state.isSubmitting]}
586
- children={([canSubmit, isSubmitting]) => (
587
- <button type="submit" disabled={!canSubmit}>
588
- {isSubmitting ? '...' : 'Submit'}
589
- </button>
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
+ </>
590
579
  )}
591
580
  />
592
- <button
593
- type="button"
594
- onClick={() => setShowField((prev) => !prev)}
595
- >
596
- {showField ? 'Hide field' : 'Show field'}
597
- </button>
598
- </form>
599
- </form.Provider>
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>
600
594
  </div>
601
595
  )
602
596
  }
@@ -613,4 +607,84 @@ describe('useField', () => {
613
607
  await user.click(getByText('Submit'))
614
608
  expect(queryByText('A first name is required')).not.toBeInTheDocument()
615
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
+ })
616
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', () => {
@@ -1,8 +1,7 @@
1
- /// <reference lib="dom" />
2
- import '@testing-library/jest-dom'
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 * as React from 'react'
6
5
  import { createFormFactory, useForm } from '../index'
7
6
  import { sleep } from './utils'
8
7
 
@@ -21,7 +20,7 @@ describe('useForm', () => {
21
20
  const form = formFactory.useForm()
22
21
 
23
22
  return (
24
- <form.Provider>
23
+ <>
25
24
  <form.Field
26
25
  name="firstName"
27
26
  defaultValue={''}
@@ -36,7 +35,7 @@ describe('useForm', () => {
36
35
  )
37
36
  }}
38
37
  />
39
- </form.Provider>
38
+ </>
40
39
  )
41
40
  }
42
41
 
@@ -64,14 +63,14 @@ describe('useForm', () => {
64
63
  })
65
64
 
66
65
  return (
67
- <form.Provider>
66
+ <>
68
67
  <form.Field
69
68
  name="firstName"
70
69
  children={(field) => {
71
70
  return <p>{field.state.value}</p>
72
71
  }}
73
72
  />
74
- </form.Provider>
73
+ </>
75
74
  )
76
75
  }
77
76
 
@@ -96,7 +95,7 @@ describe('useForm', () => {
96
95
  })
97
96
 
98
97
  return (
99
- <form.Provider>
98
+ <>
100
99
  <form.Field
101
100
  name="firstName"
102
101
  children={(field) => {
@@ -112,7 +111,7 @@ describe('useForm', () => {
112
111
  />
113
112
  <button onClick={form.handleSubmit}>Submit</button>
114
113
  {submittedData && <p>Submitted data: {submittedData.firstName}</p>}
115
- </form.Provider>
114
+ </>
116
115
  )
117
116
  }
118
117
 
@@ -146,9 +145,9 @@ describe('useForm', () => {
146
145
  return (
147
146
  <>
148
147
  {mountForm ? (
149
- <form.Provider>
148
+ <>
150
149
  <h1>{formMounted ? 'Form mounted' : 'Not mounted'}</h1>
151
- </form.Provider>
150
+ </>
152
151
  ) : (
153
152
  <button onClick={() => setMountForm(true)}>Mount form</button>
154
153
  )}
@@ -184,7 +183,7 @@ describe('useForm', () => {
184
183
  })
185
184
  const onChangeError = form.useStore((s) => s.errorMap.onChange)
186
185
  return (
187
- <form.Provider>
186
+ <>
188
187
  <form.Field
189
188
  name="firstName"
190
189
  children={(field) => (
@@ -198,7 +197,7 @@ describe('useForm', () => {
198
197
  )}
199
198
  />
200
199
  <p>{onChangeError}</p>
201
- </form.Provider>
200
+ </>
202
201
  )
203
202
  }
204
203
 
@@ -233,7 +232,7 @@ describe('useForm', () => {
233
232
 
234
233
  const errors = form.useStore((s) => s.errors)
235
234
  return (
236
- <form.Provider>
235
+ <>
237
236
  <form.Field
238
237
  name="firstName"
239
238
  children={(field) => (
@@ -249,7 +248,7 @@ describe('useForm', () => {
249
248
  </div>
250
249
  )}
251
250
  />
252
- </form.Provider>
251
+ </>
253
252
  )
254
253
  }
255
254
 
@@ -281,7 +280,7 @@ describe('useForm', () => {
281
280
  })
282
281
  const errors = form.useStore((s) => s.errorMap)
283
282
  return (
284
- <form.Provider>
283
+ <>
285
284
  <form.Field
286
285
  name="firstName"
287
286
  defaultMeta={{ isTouched: true }}
@@ -298,7 +297,7 @@ describe('useForm', () => {
298
297
  </div>
299
298
  )}
300
299
  />
301
- </form.Provider>
300
+ </>
302
301
  )
303
302
  }
304
303
 
@@ -332,7 +331,7 @@ describe('useForm', () => {
332
331
 
333
332
  const errors = form.useStore((s) => s.errorMap)
334
333
  return (
335
- <form.Provider>
334
+ <>
336
335
  <form.Field
337
336
  name="firstName"
338
337
  defaultMeta={{ isTouched: true }}
@@ -350,7 +349,7 @@ describe('useForm', () => {
350
349
  </div>
351
350
  )}
352
351
  />
353
- </form.Provider>
352
+ </>
354
353
  )
355
354
  }
356
355
  const { getByTestId, getByText, queryByText } = render(<Comp />)
@@ -387,7 +386,7 @@ describe('useForm', () => {
387
386
  })
388
387
  const errors = form.useStore((s) => s.errorMap)
389
388
  return (
390
- <form.Provider>
389
+ <>
391
390
  <form.Field
392
391
  name="firstName"
393
392
  defaultMeta={{ isTouched: true }}
@@ -404,7 +403,7 @@ describe('useForm', () => {
404
403
  </div>
405
404
  )}
406
405
  />
407
- </form.Provider>
406
+ </>
408
407
  )
409
408
  }
410
409
 
@@ -446,7 +445,7 @@ describe('useForm', () => {
446
445
  const errors = form.useStore((s) => s.errorMap)
447
446
 
448
447
  return (
449
- <form.Provider>
448
+ <>
450
449
  <form.Field
451
450
  name="firstName"
452
451
  defaultMeta={{ isTouched: true }}
@@ -464,7 +463,7 @@ describe('useForm', () => {
464
463
  </div>
465
464
  )}
466
465
  />
467
- </form.Provider>
466
+ </>
468
467
  )
469
468
  }
470
469
 
@@ -508,7 +507,7 @@ describe('useForm', () => {
508
507
  const errors = form.useStore((s) => s.errors)
509
508
 
510
509
  return (
511
- <form.Provider>
510
+ <>
512
511
  <form.Field
513
512
  name="firstName"
514
513
  defaultMeta={{ isTouched: true }}
@@ -525,7 +524,7 @@ describe('useForm', () => {
525
524
  </div>
526
525
  )}
527
526
  />
528
- </form.Provider>
527
+ </>
529
528
  )
530
529
  }
531
530
 
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  DeepKeys,
3
3
  DeepValue,
4
- FieldOptions,
4
+ FieldApiOptions,
5
5
  Validator,
6
6
  } from '@tanstack/form-core'
7
7
 
@@ -15,6 +15,12 @@ export type UseFieldOptions<
15
15
  | Validator<TParentData, unknown>
16
16
  | undefined = undefined,
17
17
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
18
- > = FieldOptions<TParentData, TName, TFieldValidator, TFormValidator, TData> & {
18
+ > = FieldApiOptions<
19
+ TParentData,
20
+ TName,
21
+ TFieldValidator,
22
+ TFormValidator,
23
+ TData
24
+ > & {
19
25
  mode?: 'value' | 'array'
20
26
  }