@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.
- package/dist/cjs/createFormFactory.cjs.map +1 -1
- package/dist/cjs/createFormFactory.d.cts +3 -3
- package/dist/cjs/types.d.cts +2 -2
- package/dist/cjs/useField.cjs +5 -28
- package/dist/cjs/useField.cjs.map +1 -1
- package/dist/cjs/useField.d.cts +4 -12
- package/dist/cjs/useForm.cjs +4 -6
- package/dist/cjs/useForm.cjs.map +1 -1
- package/dist/cjs/useForm.d.cts +14 -4
- package/dist/esm/createFormFactory.d.ts +3 -3
- package/dist/esm/createFormFactory.js.map +1 -1
- package/dist/esm/types.d.ts +2 -2
- package/dist/esm/useField.d.ts +4 -12
- package/dist/esm/useField.js +7 -30
- package/dist/esm/useField.js.map +1 -1
- package/dist/esm/useForm.d.ts +14 -4
- package/dist/esm/useForm.js +4 -6
- package/dist/esm/useForm.js.map +1 -1
- package/package.json +3 -3
- package/src/createFormFactory.ts +4 -4
- package/src/tests/createFormFactory.test.tsx +4 -5
- package/src/tests/useField.test-d.tsx +5 -5
- package/src/tests/useField.test.tsx +257 -32
- package/src/tests/useForm.test-d.tsx +1 -1
- package/src/tests/useForm.test.tsx +48 -25
- package/src/types.ts +8 -2
- package/src/useField.tsx +26 -68
- package/src/useForm.tsx +19 -14
- package/dist/cjs/formContext.cjs +0 -14
- package/dist/cjs/formContext.cjs.map +0 -1
- package/dist/cjs/formContext.d.cts +0 -10
- package/dist/cjs/useIsomorphicEffectOnce.cjs +0 -30
- package/dist/cjs/useIsomorphicEffectOnce.cjs.map +0 -1
- package/dist/cjs/useIsomorphicEffectOnce.d.cts +0 -5
- package/dist/esm/formContext.d.ts +0 -10
- package/dist/esm/formContext.js +0 -14
- package/dist/esm/formContext.js.map +0 -1
- package/dist/esm/useIsomorphicEffectOnce.d.ts +0 -5
- package/dist/esm/useIsomorphicEffectOnce.js +0 -30
- package/dist/esm/useIsomorphicEffectOnce.js.map +0 -1
- package/src/formContext.ts +0 -17
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
</>
|
|
45
44
|
)
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -67,7 +66,7 @@ describe('useField', () => {
|
|
|
67
66
|
})
|
|
68
67
|
|
|
69
68
|
return (
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|