@teamnovu/kit-vue-forms 0.0.1

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 (45) hide show
  1. package/PLAN.md +209 -0
  2. package/dist/composables/useField.d.ts +12 -0
  3. package/dist/composables/useFieldRegistry.d.ts +15 -0
  4. package/dist/composables/useForm.d.ts +10 -0
  5. package/dist/composables/useFormState.d.ts +7 -0
  6. package/dist/composables/useSubform.d.ts +5 -0
  7. package/dist/composables/useValidation.d.ts +30 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.mjs +414 -0
  10. package/dist/types/form.d.ts +41 -0
  11. package/dist/types/util.d.ts +26 -0
  12. package/dist/types/validation.d.ts +16 -0
  13. package/dist/utils/general.d.ts +2 -0
  14. package/dist/utils/path.d.ts +11 -0
  15. package/dist/utils/type-helpers.d.ts +3 -0
  16. package/dist/utils/validation.d.ts +3 -0
  17. package/dist/utils/zod.d.ts +3 -0
  18. package/package.json +41 -0
  19. package/src/composables/useField.ts +74 -0
  20. package/src/composables/useFieldRegistry.ts +53 -0
  21. package/src/composables/useForm.ts +54 -0
  22. package/src/composables/useFormData.ts +16 -0
  23. package/src/composables/useFormState.ts +21 -0
  24. package/src/composables/useSubform.ts +173 -0
  25. package/src/composables/useValidation.ts +227 -0
  26. package/src/index.ts +11 -0
  27. package/src/types/form.ts +58 -0
  28. package/src/types/util.ts +73 -0
  29. package/src/types/validation.ts +22 -0
  30. package/src/utils/general.ts +7 -0
  31. package/src/utils/path.ts +87 -0
  32. package/src/utils/type-helpers.ts +3 -0
  33. package/src/utils/validation.ts +66 -0
  34. package/src/utils/zod.ts +24 -0
  35. package/tests/formState.test.ts +138 -0
  36. package/tests/integration.test.ts +200 -0
  37. package/tests/nestedPath.test.ts +651 -0
  38. package/tests/path-utils.test.ts +159 -0
  39. package/tests/subform.test.ts +1348 -0
  40. package/tests/useField.test.ts +147 -0
  41. package/tests/useForm.test.ts +178 -0
  42. package/tests/useValidation.test.ts +216 -0
  43. package/tsconfig.json +18 -0
  44. package/vite.config.js +39 -0
  45. package/vitest.config.ts +14 -0
@@ -0,0 +1,1348 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { nextTick } from 'vue'
3
+ import { z } from 'zod'
4
+ import { useForm } from '../src/composables/useForm'
5
+ import { Form } from '../src/types/form'
6
+
7
+ describe('Subform Implementation', () => {
8
+ describe('Basic Functionality', () => {
9
+ describe('Subform Creation', () => {
10
+ it('should create subform with correct data scoping', () => {
11
+ const form = useForm({
12
+ initialData: {
13
+ user: {
14
+ name: 'John',
15
+ email: 'john@example.com',
16
+ },
17
+ company: { name: 'Tech Corp' },
18
+ },
19
+ })
20
+
21
+ const userForm = form.getSubForm('user')
22
+
23
+ expect(userForm.formData.value).toEqual({
24
+ name: 'John',
25
+ email: 'john@example.com',
26
+ })
27
+ expect(userForm.initialData.value).toEqual({
28
+ name: 'John',
29
+ email: 'john@example.com',
30
+ })
31
+ })
32
+
33
+ it('should create array subforms', () => {
34
+ const form = useForm({
35
+ initialData: {
36
+ users: [
37
+ {
38
+ name: 'John',
39
+ email: 'john@example.com',
40
+ },
41
+ {
42
+ name: 'Jane',
43
+ email: 'jane@example.com',
44
+ },
45
+ ],
46
+ },
47
+ })
48
+
49
+ const firstUserForm = form.getSubForm('users.0')
50
+ const secondUserForm = form.getSubForm('users.1')
51
+
52
+ expect(firstUserForm.formData.value).toEqual({
53
+ name: 'John',
54
+ email: 'john@example.com',
55
+ })
56
+ expect(secondUserForm.formData.value).toEqual({
57
+ name: 'Jane',
58
+ email: 'jane@example.com',
59
+ })
60
+ })
61
+
62
+ it('should handle deeply nested subforms', () => {
63
+ const form = useForm({
64
+ initialData: {
65
+ user: {
66
+ profile: {
67
+ personal: {
68
+ name: 'John',
69
+ age: 30,
70
+ },
71
+ work: {
72
+ title: 'Developer',
73
+ company: 'Tech Corp',
74
+ },
75
+ },
76
+ },
77
+ },
78
+ })
79
+
80
+ const userForm = form.getSubForm('user')
81
+ const profileForm = userForm.getSubForm('profile')
82
+ const personalForm = profileForm.getSubForm('personal')
83
+
84
+ expect(personalForm.formData.value).toEqual({
85
+ name: 'John',
86
+ age: 30,
87
+ })
88
+ })
89
+ })
90
+
91
+ describe('Field Operations', () => {
92
+ it('should register fields with correct paths in main form', () => {
93
+ const form = useForm({
94
+ initialData: {
95
+ user: { name: 'John' },
96
+ },
97
+ })
98
+
99
+ const userForm = form.getSubForm('user')
100
+ const nameField = userForm.defineField({ path: 'name' })
101
+
102
+ expect(nameField.path.value).toBe('name')
103
+ expect(nameField.value.value).toBe('John')
104
+ expect(form.getField('user.name')).toBeDefined()
105
+ })
106
+
107
+ it('should retrieve fields with path transformation', () => {
108
+ const form = useForm({
109
+ initialData: {
110
+ user: {
111
+ name: 'John',
112
+ email: 'john@example.com',
113
+ },
114
+ },
115
+ })
116
+
117
+ const userForm = form.getSubForm('user')
118
+ userForm.defineField({ path: 'name' })
119
+ userForm.defineField({ path: 'email' })
120
+
121
+ const nameField = userForm.getField('name')
122
+ const emailField = userForm.getField('email')
123
+
124
+ expect(nameField).toBeDefined()
125
+ expect(emailField).toBeDefined()
126
+ expect(nameField?.path.value).toBe('name')
127
+ expect(emailField?.path.value).toBe('email')
128
+ })
129
+
130
+ it('should handle field operations on nested subforms', () => {
131
+ const form = useForm({
132
+ initialData: {
133
+ user: {
134
+ profile: {
135
+ name: 'John',
136
+ bio: 'Developer',
137
+ },
138
+ },
139
+ },
140
+ })
141
+
142
+ const userForm = form.getSubForm('user')
143
+ const profileForm = userForm.getSubForm('profile')
144
+ const nameField = profileForm.defineField({ path: 'name' })
145
+
146
+ expect(nameField.path.value).toBe('name')
147
+ expect(nameField.value.value).toBe('John')
148
+
149
+ // Should be registered in main form with full path
150
+ expect(form.getField('user.profile.name')).toBeDefined()
151
+ })
152
+
153
+ it('should handle field value updates correctly', async () => {
154
+ const form = useForm({
155
+ initialData: {
156
+ user: { name: 'John' },
157
+ },
158
+ })
159
+
160
+ const userForm = form.getSubForm('user')
161
+ const nameField = userForm.defineField({ path: 'name' })
162
+
163
+ nameField.setValue('Jane')
164
+ await nextTick()
165
+
166
+ expect(nameField.value.value).toBe('Jane')
167
+ expect(userForm.formData.value.name).toBe('Jane')
168
+ expect(form.formData.value.user.name).toBe('Jane')
169
+ })
170
+ })
171
+
172
+ describe('Nested Subforms', () => {
173
+ it('should create nested subforms from subforms', () => {
174
+ const form = useForm({
175
+ initialData: {
176
+ user: {
177
+ profile: {
178
+ name: 'John',
179
+ settings: {
180
+ theme: 'dark',
181
+ notifications: true,
182
+ },
183
+ },
184
+ },
185
+ },
186
+ })
187
+
188
+ const userForm = form.getSubForm('user')
189
+ const profileForm = userForm.getSubForm('profile')
190
+ const settingsForm = profileForm.getSubForm('settings')
191
+
192
+ expect(settingsForm.formData.value).toEqual({
193
+ theme: 'dark',
194
+ notifications: true,
195
+ })
196
+ })
197
+
198
+ it('should handle infinite nesting levels', () => {
199
+ const form = useForm({
200
+ initialData: {
201
+ level1: {
202
+ level2: {
203
+ level3: {
204
+ level4: {
205
+ level5: {
206
+ name: 'Deep Value',
207
+ },
208
+ },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ })
214
+
215
+ const level1Form = form.getSubForm('level1')
216
+ const level2Form = level1Form.getSubForm('level2')
217
+ const level3Form = level2Form.getSubForm('level3')
218
+ const level4Form = level3Form.getSubForm('level4')
219
+ const level5Form = level4Form.getSubForm('level5')
220
+
221
+ expect(level5Form.formData.value).toEqual({
222
+ name: 'Deep Value',
223
+ })
224
+
225
+ const nameField = level5Form.defineField({ path: 'name' })
226
+ expect(form.getField('level1.level2.level3.level4.level5.name')).toBeDefined()
227
+ })
228
+
229
+ it('should handle mixed object and array nesting', () => {
230
+ const form = useForm({
231
+ initialData: {
232
+ teams: [
233
+ {
234
+ name: 'Team A',
235
+ members: [
236
+ {
237
+ name: 'John',
238
+ role: 'lead',
239
+ },
240
+ {
241
+ name: 'Jane',
242
+ role: 'dev',
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ },
248
+ })
249
+
250
+ const teamForm = form.getSubForm('teams.0')
251
+ const memberForm = teamForm.getSubForm('members.0')
252
+
253
+ expect(memberForm.formData.value).toEqual({
254
+ name: 'John',
255
+ role: 'lead',
256
+ })
257
+ })
258
+ })
259
+ })
260
+
261
+ describe('Validation Integration', () => {
262
+ describe('Schema Validation', () => {
263
+ it('should validate subform with schema using defineValidator', async () => {
264
+ const form = useForm({
265
+ initialData: {
266
+ user: {
267
+ name: '',
268
+ email: '',
269
+ },
270
+ },
271
+ })
272
+
273
+ const userForm = form.getSubForm('user')
274
+ userForm.defineValidator({
275
+ schema: z.object({
276
+ name: z.string().min(1, 'Name required'),
277
+ email: z.string().email('Invalid email'),
278
+ }),
279
+ })
280
+
281
+ // Set invalid data
282
+ userForm.formData.value.name = ''
283
+ userForm.formData.value.email = 'invalid'
284
+
285
+ const result = await form.validateForm()
286
+
287
+ expect(result.isValid).toBe(false)
288
+ expect(result.errors.propertyErrors['user.name']).toContain('Name required')
289
+ expect(result.errors.propertyErrors['user.email']).toContain('Invalid email')
290
+ })
291
+
292
+ it('should validate nested subforms with schemas', async () => {
293
+ const form = useForm({
294
+ initialData: {
295
+ user: {
296
+ profile: {
297
+ name: '',
298
+ bio: '',
299
+ },
300
+ },
301
+ },
302
+ })
303
+
304
+ const userForm = form.getSubForm('user')
305
+ const profileForm = userForm.getSubForm('profile')
306
+
307
+ profileForm.defineValidator({
308
+ schema: z.object({
309
+ name: z.string().min(1, 'Name required'),
310
+ bio: z.string().min(10, 'Bio too short'),
311
+ }),
312
+ })
313
+
314
+ // Set invalid data
315
+ profileForm.formData.value.name = ''
316
+ profileForm.formData.value.bio = 'Short'
317
+
318
+ const result = await form.validateForm()
319
+
320
+ expect(result.isValid).toBe(false)
321
+ expect(result.errors.propertyErrors['user.profile.name']).toContain('Name required')
322
+ expect(result.errors.propertyErrors['user.profile.bio']).toContain('Bio too short')
323
+ })
324
+
325
+ it('should handle multiple subform validations at same level', async () => {
326
+ const form = useForm({
327
+ initialData: {
328
+ user: { name: '' },
329
+ company: { name: '' },
330
+ },
331
+ })
332
+
333
+ const userForm = form.getSubForm('user')
334
+ const companyForm = form.getSubForm('company')
335
+
336
+ userForm.defineValidator({
337
+ schema: z.object({
338
+ name: z.string().min(1, 'User name required'),
339
+ }),
340
+ })
341
+
342
+ companyForm.defineValidator({
343
+ schema: z.object({
344
+ name: z.string().min(1, 'Company name required'),
345
+ }),
346
+ })
347
+
348
+ const result = await form.validateForm()
349
+
350
+ expect(result.isValid).toBe(false)
351
+ expect(result.errors.propertyErrors['user.name']).toContain('User name required')
352
+ expect(result.errors.propertyErrors['company.name']).toContain('Company name required')
353
+ })
354
+ })
355
+
356
+ describe('Custom Validation Functions', () => {
357
+ it('should validate subform with custom validation function', async () => {
358
+ const form = useForm({
359
+ initialData: {
360
+ user: {
361
+ name: 'admin',
362
+ email: 'admin@example.com',
363
+ },
364
+ },
365
+ })
366
+
367
+ const userForm = form.getSubForm('user')
368
+ userForm.defineValidator({
369
+ validateFn: async (data) => {
370
+ const errors: Record<string, string[]> = {}
371
+
372
+ if (data.name === 'admin') {
373
+ errors.name = ['Admin name not allowed']
374
+ }
375
+
376
+ return {
377
+ isValid: Object.keys(errors).length === 0,
378
+ errors: {
379
+ general: [],
380
+ propertyErrors: errors,
381
+ },
382
+ }
383
+ },
384
+ })
385
+
386
+ const result = await form.validateForm()
387
+
388
+ expect(result.isValid).toBe(false)
389
+ expect(result.errors.propertyErrors['user.name']).toContain('Admin name not allowed')
390
+ })
391
+
392
+ it('should handle both schema and custom validation', async () => {
393
+ const form = useForm({
394
+ initialData: {
395
+ user: {
396
+ name: '',
397
+ email: 'test@example.com',
398
+ },
399
+ },
400
+ })
401
+
402
+ const userForm = form.getSubForm('user')
403
+ userForm.defineValidator({
404
+ schema: z.object({
405
+ name: z.string().min(1, 'Name required'),
406
+ }),
407
+ validateFn: async (data) => {
408
+ const errors: Record<string, string[]> = {}
409
+
410
+ if (data.email === 'test@example.com') {
411
+ errors.email = ['Test email not allowed']
412
+ }
413
+
414
+ return {
415
+ isValid: Object.keys(errors).length === 0,
416
+ errors: {
417
+ general: [],
418
+ propertyErrors: errors,
419
+ },
420
+ }
421
+ },
422
+ })
423
+
424
+ const result = await form.validateForm()
425
+
426
+ expect(result.isValid).toBe(false)
427
+ expect(result.errors.propertyErrors['user.name']).toContain('Name required')
428
+ expect(result.errors.propertyErrors['user.email']).toContain('Test email not allowed')
429
+ })
430
+
431
+ it('should handle validation with nested custom functions', async () => {
432
+ const form = useForm({
433
+ initialData: {
434
+ user: {
435
+ profile: {
436
+ name: 'admin',
437
+ bio: 'Test bio',
438
+ },
439
+ },
440
+ },
441
+ })
442
+
443
+ const userForm = form.getSubForm('user')
444
+ const profileForm = userForm.getSubForm('profile')
445
+
446
+ profileForm.defineValidator({
447
+ validateFn: async (data) => {
448
+ const errors: Record<string, string[]> = {}
449
+
450
+ if (data.name === 'admin') {
451
+ errors.name = ['Admin profile name not allowed']
452
+ }
453
+
454
+ return {
455
+ isValid: Object.keys(errors).length === 0,
456
+ errors: {
457
+ general: [],
458
+ propertyErrors: errors,
459
+ },
460
+ }
461
+ },
462
+ })
463
+
464
+ const result = await form.validateForm()
465
+
466
+ expect(result.isValid).toBe(false)
467
+ expect(result.errors.propertyErrors['user.profile.name']).toContain('Admin profile name not allowed')
468
+ })
469
+ })
470
+
471
+ describe('Main Form and Subform Validation Integration', () => {
472
+ it('should integrate main form and subform validation', async () => {
473
+ const form = useForm({
474
+ initialData: {
475
+ globalSetting: 'abc',
476
+ user: {
477
+ name: 'admin',
478
+ },
479
+ },
480
+ })
481
+
482
+ // Main form validation
483
+ form.defineValidator({
484
+ schema: z.object({
485
+ globalSetting: z.string().min(5, 'Global setting too short'),
486
+ }),
487
+ })
488
+
489
+ // Subform validation
490
+ const userForm = form.getSubForm('user')
491
+ userForm.defineValidator({
492
+ validateFn: async (data) => {
493
+ const errors: Record<string, string[]> = {}
494
+
495
+ if (data.name === 'admin') {
496
+ errors.name = ['Admin name not allowed']
497
+ }
498
+
499
+ return {
500
+ isValid: Object.keys(errors).length === 0,
501
+ errors: {
502
+ general: [],
503
+ propertyErrors: errors,
504
+ },
505
+ }
506
+ },
507
+ })
508
+
509
+ const result = await form.validateForm()
510
+
511
+ expect(result.isValid).toBe(false)
512
+ expect(result.errors.propertyErrors['globalSetting']).toContain('Global setting too short')
513
+ expect(result.errors.propertyErrors['user.name']).toContain('Admin name not allowed')
514
+ })
515
+ })
516
+ })
517
+
518
+ describe('State Management', () => {
519
+ describe('Dirty State', () => {
520
+ it('should compute isDirty for subform scope only', async () => {
521
+ const form = useForm({
522
+ initialData: {
523
+ user: { name: 'John' },
524
+ company: { name: 'Tech Corp' },
525
+ },
526
+ })
527
+
528
+ const userForm = form.getSubForm('user')
529
+ const companyForm = form.getSubForm('company')
530
+
531
+ // Define fields to enable dirty state computation
532
+ const userNameField = userForm.defineField({ path: 'name' })
533
+ const companyNameField = companyForm.defineField({ path: 'name' })
534
+
535
+ expect(userForm.isDirty.value).toBe(false)
536
+ expect(companyForm.isDirty.value).toBe(false)
537
+
538
+ userNameField.setValue('Jane')
539
+ await nextTick()
540
+
541
+ expect(userForm.isDirty.value).toBe(true)
542
+ expect(companyForm.isDirty.value).toBe(false)
543
+ })
544
+
545
+ it('should handle nested isDirty computation', async () => {
546
+ const form = useForm({
547
+ initialData: {
548
+ user: {
549
+ profile: {
550
+ name: 'John',
551
+ bio: 'Developer',
552
+ },
553
+ },
554
+ },
555
+ })
556
+
557
+ const userForm = form.getSubForm('user')
558
+ const profileForm = userForm.getSubForm('profile')
559
+
560
+ // Define fields to enable dirty state computation
561
+ const nameField = profileForm.defineField({ path: 'name' })
562
+
563
+ expect(profileForm.isDirty.value).toBe(false)
564
+
565
+ nameField.setValue('Jane')
566
+ await nextTick()
567
+
568
+ expect(profileForm.isDirty.value).toBe(true)
569
+ expect(userForm.isDirty.value).toBe(true)
570
+ })
571
+
572
+ it('should handle array subform isDirty', async () => {
573
+ const form = useForm({
574
+ initialData: {
575
+ users: [
576
+ { name: 'John' },
577
+ { name: 'Jane' },
578
+ ],
579
+ },
580
+ })
581
+
582
+ const firstUserForm = form.getSubForm('users.0')
583
+ const secondUserForm = form.getSubForm('users.1')
584
+
585
+ // Define fields to enable dirty state computation
586
+ const firstNameField = firstUserForm.defineField({ path: 'name' })
587
+ const secondNameField = secondUserForm.defineField({ path: 'name' })
588
+
589
+ expect(firstUserForm.isDirty.value).toBe(false)
590
+ expect(secondUserForm.isDirty.value).toBe(false)
591
+
592
+ firstNameField.setValue('Johnny')
593
+ await nextTick()
594
+
595
+ expect(firstUserForm.isDirty.value).toBe(true)
596
+ expect(secondUserForm.isDirty.value).toBe(false)
597
+ })
598
+ })
599
+
600
+ describe('Touched State', () => {
601
+ it('should compute isTouched for subform scope only', async () => {
602
+ const form = useForm({
603
+ initialData: {
604
+ user: { name: 'John' },
605
+ company: { name: 'Tech Corp' },
606
+ },
607
+ })
608
+
609
+ const userForm = form.getSubForm('user')
610
+ const companyForm = form.getSubForm('company')
611
+
612
+ const userNameField = userForm.defineField({ path: 'name' })
613
+ const companyNameField = companyForm.defineField({ path: 'name' })
614
+
615
+ expect(userForm.isTouched.value).toBe(false)
616
+ expect(companyForm.isTouched.value).toBe(false)
617
+
618
+ userNameField.onBlur()
619
+ await nextTick()
620
+
621
+ expect(userForm.isTouched.value).toBe(true)
622
+ expect(companyForm.isTouched.value).toBe(false)
623
+ })
624
+
625
+ it('should handle nested isTouched computation', async () => {
626
+ const form = useForm({
627
+ initialData: {
628
+ user: {
629
+ profile: {
630
+ name: 'John',
631
+ bio: 'Developer',
632
+ },
633
+ },
634
+ },
635
+ })
636
+
637
+ const userForm = form.getSubForm('user')
638
+ const profileForm = userForm.getSubForm('profile')
639
+
640
+ const nameField = profileForm.defineField({ path: 'name' })
641
+
642
+ expect(profileForm.isTouched.value).toBe(false)
643
+ expect(userForm.isTouched.value).toBe(false)
644
+
645
+ nameField.onBlur()
646
+ await nextTick()
647
+
648
+ expect(profileForm.isTouched.value).toBe(true)
649
+ expect(userForm.isTouched.value).toBe(true)
650
+ })
651
+
652
+ it('should handle multiple fields touched in same subform', async () => {
653
+ const form = useForm({
654
+ initialData: {
655
+ user: {
656
+ name: 'John',
657
+ email: 'john@example.com',
658
+ },
659
+ },
660
+ })
661
+
662
+ const userForm = form.getSubForm('user')
663
+ const nameField = userForm.defineField({ path: 'name' })
664
+ const emailField = userForm.defineField({ path: 'email' })
665
+
666
+ expect(userForm.isTouched.value).toBe(false)
667
+
668
+ nameField.onBlur()
669
+ await nextTick()
670
+
671
+ expect(userForm.isTouched.value).toBe(true)
672
+
673
+ emailField.onBlur()
674
+ await nextTick()
675
+
676
+ expect(userForm.isTouched.value).toBe(true)
677
+ })
678
+ })
679
+
680
+ describe('Error State', () => {
681
+ it('should filter errors to subform scope', async () => {
682
+ const form = useForm({
683
+ initialData: {
684
+ user: { name: '' },
685
+ company: { name: '' },
686
+ },
687
+ })
688
+
689
+ const userForm = form.getSubForm('user')
690
+ const companyForm = form.getSubForm('company')
691
+
692
+ userForm.defineValidator({
693
+ schema: z.object({
694
+ name: z.string().min(1, 'User name required'),
695
+ }),
696
+ })
697
+
698
+ companyForm.defineValidator({
699
+ schema: z.object({
700
+ name: z.string().min(1, 'Company name required'),
701
+ }),
702
+ })
703
+
704
+ await form.validateForm()
705
+
706
+ // User form should only see user errors
707
+ expect(userForm.errors.value.propertyErrors['name']).toContain('User name required')
708
+ expect(userForm.errors.value.propertyErrors['company.name']).toBeUndefined()
709
+
710
+ // Company form should only see company errors
711
+ expect(companyForm.errors.value.propertyErrors['name']).toContain('Company name required')
712
+ expect(companyForm.errors.value.propertyErrors['user.name']).toBeUndefined()
713
+ })
714
+
715
+ it('should handle nested error filtering', async () => {
716
+ const form = useForm({
717
+ initialData: {
718
+ user: {
719
+ profile: {
720
+ name: '',
721
+ bio: '',
722
+ },
723
+ settings: {
724
+ theme: '',
725
+ },
726
+ },
727
+ },
728
+ })
729
+
730
+ const userForm = form.getSubForm('user')
731
+ const profileForm = userForm.getSubForm('profile')
732
+ const settingsForm = userForm.getSubForm('settings')
733
+
734
+ profileForm.defineValidator({
735
+ schema: z.object({
736
+ name: z.string().min(1, 'Name required'),
737
+ bio: z.string().min(1, 'Bio required'),
738
+ }),
739
+ })
740
+
741
+ settingsForm.defineValidator({
742
+ schema: z.object({
743
+ theme: z.string().min(1, 'Theme required'),
744
+ }),
745
+ })
746
+
747
+ await form.validateForm()
748
+
749
+ // Profile form should only see profile errors
750
+ expect(profileForm.errors.value.propertyErrors['name']).toContain('Name required')
751
+ expect(profileForm.errors.value.propertyErrors['bio']).toContain('Bio required')
752
+ expect(profileForm.errors.value.propertyErrors['settings.theme']).toBeUndefined()
753
+
754
+ // Settings form should only see settings errors
755
+ expect(settingsForm.errors.value.propertyErrors['theme']).toContain('Theme required')
756
+ expect(settingsForm.errors.value.propertyErrors['profile.name']).toBeUndefined()
757
+ })
758
+
759
+ it('should handle general errors in subforms', async () => {
760
+ const form = useForm({
761
+ initialData: {
762
+ user: {
763
+ name: 'test',
764
+ },
765
+ },
766
+ })
767
+
768
+ const userForm = form.getSubForm('user')
769
+ userForm.defineValidator({
770
+ validateFn: async () => ({
771
+ isValid: false,
772
+ errors: {
773
+ general: ['General user error'],
774
+ propertyErrors: {},
775
+ },
776
+ }),
777
+ })
778
+
779
+ await form.validateForm()
780
+
781
+ expect(userForm.errors.value.general).toContain('General user error')
782
+ })
783
+ })
784
+
785
+ describe('Reset Functionality', () => {
786
+ it('should reset only subform fields', async () => {
787
+ const form = useForm({
788
+ initialData: {
789
+ user: { name: 'John' },
790
+ company: { name: 'Tech Corp' },
791
+ },
792
+ })
793
+
794
+ const userForm = form.getSubForm('user')
795
+ const companyForm = form.getSubForm('company')
796
+
797
+ const userNameField = userForm.defineField({ path: 'name' })
798
+ const companyNameField = companyForm.defineField({ path: 'name' })
799
+
800
+ // Change values
801
+ userNameField.setValue('Jane')
802
+ companyNameField.setValue('New Corp')
803
+ await nextTick()
804
+
805
+ expect(userForm.formData.value.name).toBe('Jane')
806
+ expect(companyForm.formData.value.name).toBe('New Corp')
807
+
808
+ // Reset only user subform
809
+ userForm.reset()
810
+ await nextTick()
811
+
812
+ expect(userForm.formData.value.name).toBe('John')
813
+ expect(companyForm.formData.value.name).toBe('New Corp')
814
+ })
815
+
816
+ it('should handle nested subform reset', async () => {
817
+ const form = useForm({
818
+ initialData: {
819
+ user: {
820
+ profile: {
821
+ name: 'John',
822
+ bio: 'Developer',
823
+ },
824
+ },
825
+ },
826
+ })
827
+
828
+ const userForm = form.getSubForm('user')
829
+ const profileForm = userForm.getSubForm('profile')
830
+
831
+ const nameField = profileForm.defineField({ path: 'name' })
832
+ const bioField = profileForm.defineField({ path: 'bio' })
833
+
834
+ // Change values
835
+ nameField.setValue('Jane')
836
+ bioField.setValue('Designer')
837
+ await nextTick()
838
+
839
+ expect(profileForm.formData.value.name).toBe('Jane')
840
+ expect(profileForm.formData.value.bio).toBe('Designer')
841
+
842
+ // Reset profile subform
843
+ profileForm.reset()
844
+ await nextTick()
845
+
846
+ expect(profileForm.formData.value.name).toBe('John')
847
+ expect(profileForm.formData.value.bio).toBe('Developer')
848
+ })
849
+
850
+ it('should handle array subform reset', async () => {
851
+ const form = useForm({
852
+ initialData: {
853
+ users: [
854
+ { name: 'John' },
855
+ { name: 'Jane' },
856
+ ],
857
+ },
858
+ })
859
+
860
+ const firstUserForm = form.getSubForm('users.0')
861
+ const secondUserForm = form.getSubForm('users.1')
862
+
863
+ const firstNameField = firstUserForm.defineField({ path: 'name' })
864
+ const secondNameField = secondUserForm.defineField({ path: 'name' })
865
+
866
+ // Change values
867
+ firstNameField.setValue('Johnny')
868
+ secondNameField.setValue('Janie')
869
+ await nextTick()
870
+
871
+ expect(firstUserForm.formData.value.name).toBe('Johnny')
872
+ expect(secondUserForm.formData.value.name).toBe('Janie')
873
+
874
+ // Reset only first user subform
875
+ firstUserForm.reset()
876
+ await nextTick()
877
+
878
+ expect(firstUserForm.formData.value.name).toBe('John')
879
+ expect(secondUserForm.formData.value.name).toBe('Janie')
880
+ })
881
+ })
882
+
883
+ describe('State Synchronization', () => {
884
+ it('should maintain state consistency between main form and subforms', async () => {
885
+ const form = useForm({
886
+ initialData: {
887
+ user: { name: 'John' },
888
+ },
889
+ })
890
+
891
+ const userForm = form.getSubForm('user')
892
+ const nameField = userForm.defineField({ path: 'name' })
893
+
894
+ // Change through subform
895
+ userForm.formData.value.name = 'Jane'
896
+ await nextTick()
897
+
898
+ expect(form.formData.value.user.name).toBe('Jane')
899
+ expect(nameField.value.value).toBe('Jane')
900
+
901
+ // Change through main form
902
+ form.formData.value.user.name = 'Bob'
903
+ await nextTick()
904
+
905
+ expect(userForm.formData.value.name).toBe('Bob')
906
+ expect(nameField.value.value).toBe('Bob')
907
+ })
908
+
909
+ it('should handle state changes through main form', async () => {
910
+ const form = useForm({
911
+ initialData: {
912
+ user: {
913
+ profile: {
914
+ name: 'John',
915
+ },
916
+ },
917
+ },
918
+ })
919
+
920
+ const userForm = form.getSubForm('user')
921
+ const profileForm = userForm.getSubForm('profile')
922
+
923
+ // Change through main form
924
+ form.formData.value.user.profile.name = 'Jane'
925
+ await nextTick()
926
+
927
+ expect(userForm.formData.value.profile.name).toBe('Jane')
928
+ expect(profileForm.formData.value.name).toBe('Jane')
929
+ })
930
+
931
+ it('should handle initial data changes', async () => {
932
+ const initialData = {
933
+ user: { name: 'John' },
934
+ }
935
+
936
+ const form = useForm({
937
+ initialData: () => initialData,
938
+ })
939
+
940
+ const userForm = form.getSubForm('user')
941
+
942
+ expect(userForm.initialData.value.name).toBe('John')
943
+
944
+ // This test would need reactive initial data to work properly
945
+ // For now, just verify the current behavior
946
+ expect(userForm.initialData.value).toEqual({ name: 'John' })
947
+ })
948
+ })
949
+ })
950
+
951
+ describe('Edge Cases', () => {
952
+ describe('Validation Edge Cases', () => {
953
+ it('should handle validation function that throws error', async () => {
954
+ const form = useForm({
955
+ initialData: {
956
+ user: { name: 'test' },
957
+ },
958
+ })
959
+
960
+ const userForm = form.getSubForm('user')
961
+ userForm.defineValidator({
962
+ validateFn: async () => {
963
+ throw new Error('Validation function error')
964
+ },
965
+ })
966
+
967
+ const result = await form.validateForm()
968
+
969
+ expect(result.isValid).toBe(false)
970
+ expect(result.errors.general).toContain('Validation function error')
971
+ })
972
+
973
+ it('should handle validation function that returns invalid format', async () => {
974
+ const form = useForm({
975
+ initialData: {
976
+ user: { name: 'test' },
977
+ },
978
+ })
979
+
980
+ const userForm = form.getSubForm('user')
981
+ userForm.defineValidator({
982
+ validateFn: async () => {
983
+ // Return invalid format (missing propertyErrors)
984
+ return {
985
+ isValid: false,
986
+ errors: {
987
+ general: ['Invalid format error'],
988
+ propertyErrors: {},
989
+ },
990
+ }
991
+ },
992
+ })
993
+
994
+ const result = await form.validateForm()
995
+
996
+ expect(result.isValid).toBe(false)
997
+ expect(result.errors.general).toContain('Invalid format error')
998
+ })
999
+
1000
+ it('should handle validation function with undefined data', async () => {
1001
+ const form = useForm({
1002
+ initialData: {
1003
+ user: undefined,
1004
+ },
1005
+ })
1006
+
1007
+ const userForm = form.getSubForm('user' as never)
1008
+ userForm.defineValidator({
1009
+ validateFn: async (data) => {
1010
+ if (!data) {
1011
+ return {
1012
+ isValid: false,
1013
+ errors: {
1014
+ general: ['Data is undefined'],
1015
+ propertyErrors: {},
1016
+ },
1017
+ }
1018
+ }
1019
+ return {
1020
+ isValid: true,
1021
+ errors: {
1022
+ general: [],
1023
+ propertyErrors: {},
1024
+ },
1025
+ }
1026
+ },
1027
+ })
1028
+
1029
+ const result = await form.validateForm()
1030
+
1031
+ expect(result.isValid).toBe(false)
1032
+ expect(result.errors.general).toContain('Data is undefined')
1033
+ })
1034
+ })
1035
+
1036
+ describe('Path Edge Cases', () => {
1037
+ it('should handle special characters in paths', () => {
1038
+ const form = useForm({
1039
+ initialData: {
1040
+ 'user-name': { 'first-name': 'John' },
1041
+ },
1042
+ })
1043
+
1044
+ const userForm = form.getSubForm('user-name')
1045
+ expect(userForm.formData.value).toEqual({ 'first-name': 'John' })
1046
+ })
1047
+
1048
+ it('should handle numeric string paths', () => {
1049
+ const form = useForm({
1050
+ initialData: {
1051
+ 123: { name: 'test' },
1052
+ },
1053
+ })
1054
+
1055
+ const numericForm = form.getSubForm('123')
1056
+ expect(numericForm.formData.value).toEqual({ name: 'test' })
1057
+ })
1058
+ })
1059
+
1060
+ describe('Data Structure Edge Cases', () => {
1061
+ it('should handle null values in subform data', () => {
1062
+ const form = useForm({
1063
+ initialData: {
1064
+ user: {
1065
+ name: null,
1066
+ email: 'test@example.com',
1067
+ },
1068
+ },
1069
+ })
1070
+
1071
+ const userForm = form.getSubForm('user')
1072
+ expect(userForm.formData.value.name).toBe(null)
1073
+ expect(userForm.formData.value.email).toBe('test@example.com')
1074
+ })
1075
+
1076
+ it('should handle undefined values in subform data', () => {
1077
+ const form = useForm({
1078
+ initialData: {
1079
+ user: {
1080
+ name: undefined,
1081
+ email: 'test@example.com',
1082
+ },
1083
+ },
1084
+ })
1085
+
1086
+ const userForm = form.getSubForm('user')
1087
+ expect(userForm.formData.value.name).toBe(undefined)
1088
+ expect(userForm.formData.value.email).toBe('test@example.com')
1089
+ })
1090
+
1091
+ it('should handle empty objects', () => {
1092
+ const form = useForm({
1093
+ initialData: {
1094
+ user: {},
1095
+ },
1096
+ })
1097
+
1098
+ const userForm = form.getSubForm('user' as never)
1099
+ expect(userForm.formData.value).toEqual({})
1100
+ })
1101
+
1102
+ it('should handle empty arrays', () => {
1103
+ const form = useForm({
1104
+ initialData: {
1105
+ users: [],
1106
+ },
1107
+ })
1108
+
1109
+ const usersForm = form.getSubForm('users')
1110
+ expect(usersForm.formData.value).toEqual([])
1111
+ })
1112
+ })
1113
+ })
1114
+
1115
+ describe('Performance', () => {
1116
+ describe('Large Form Performance', () => {
1117
+ it('should handle large numbers of subforms efficiently', () => {
1118
+ const initialData = {
1119
+ users: [] as {
1120
+ id: number
1121
+ name: string
1122
+ email: string
1123
+ }[],
1124
+ }
1125
+
1126
+ // Create 100 users
1127
+ for (let i = 0; i < 100; i++) {
1128
+ initialData.users.push({
1129
+ id: i,
1130
+ name: `User ${i}`,
1131
+ email: `user${i}@example.com`,
1132
+ })
1133
+ }
1134
+
1135
+ const form = useForm({ initialData })
1136
+
1137
+ const startTime = performance.now()
1138
+
1139
+ // Create 100 subforms
1140
+ const subforms: Form<{
1141
+ id: number
1142
+ name: string
1143
+ email: string
1144
+ }>[] = []
1145
+ for (let i = 0; i < 100; i++) {
1146
+ subforms.push(form.getSubForm(`users.${i}`))
1147
+ }
1148
+
1149
+ const endTime = performance.now()
1150
+ const duration = endTime - startTime
1151
+
1152
+ // Should complete within reasonable time (less than 100ms)
1153
+ expect(duration).toBeLessThan(100)
1154
+ expect(subforms).toHaveLength(100)
1155
+ expect(subforms[0].formData.value.name).toBe('User 0')
1156
+ expect(subforms[99].formData.value.name).toBe('User 99')
1157
+ })
1158
+
1159
+ it('should handle rapid field registration efficiently', () => {
1160
+ const form = useForm({
1161
+ initialData: {
1162
+ user: {
1163
+ name: 'John',
1164
+ email: 'john@example.com',
1165
+ bio: 'Developer',
1166
+ phone: '123-456-7890',
1167
+ address: '123 Main St',
1168
+ },
1169
+ },
1170
+ })
1171
+
1172
+ const userForm = form.getSubForm('user')
1173
+
1174
+ const startTime = performance.now()
1175
+
1176
+ // Register many fields rapidly
1177
+ const fields = [
1178
+ userForm.defineField({ path: 'name' }),
1179
+ userForm.defineField({ path: 'email' }),
1180
+ userForm.defineField({ path: 'bio' }),
1181
+ userForm.defineField({ path: 'phone' }),
1182
+ userForm.defineField({ path: 'address' }),
1183
+ ]
1184
+
1185
+ const endTime = performance.now()
1186
+ const duration = endTime - startTime
1187
+
1188
+ // Should complete within reasonable time
1189
+ expect(duration).toBeLessThan(50)
1190
+ expect(fields).toHaveLength(5)
1191
+ expect(fields[0].value.value).toBe('John')
1192
+ })
1193
+
1194
+ it('should handle complex validation on large forms', async () => {
1195
+ const initialData = {
1196
+ users: [] as {
1197
+ id: number
1198
+ name: string
1199
+ email: string
1200
+ }[],
1201
+ }
1202
+
1203
+ // Create 50 users for validation test
1204
+ for (let i = 0; i < 50; i++) {
1205
+ initialData.users.push({
1206
+ id: i,
1207
+ name: `User ${i}`,
1208
+ email: `user${i}@example.com`,
1209
+ })
1210
+ }
1211
+
1212
+ const form = useForm({ initialData })
1213
+
1214
+ // Add validation to multiple subforms
1215
+ for (let i = 0; i < 50; i++) {
1216
+ const userForm = form.getSubForm(`users.${i}`)
1217
+ userForm.defineValidator({
1218
+ schema: z.object({
1219
+ name: z.string().min(1, 'Name required'),
1220
+ email: z.string().email('Invalid email'),
1221
+ }),
1222
+ })
1223
+ }
1224
+
1225
+ const startTime = performance.now()
1226
+ const result = await form.validateForm()
1227
+ const endTime = performance.now()
1228
+ const duration = endTime - startTime
1229
+
1230
+ // Should complete within reasonable time (less than 1000ms)
1231
+ expect(duration).toBeLessThan(1000)
1232
+ expect(result.isValid).toBe(true)
1233
+ })
1234
+ })
1235
+
1236
+ describe('Memory Usage', () => {
1237
+ it('should not create excessive memory usage with many subforms', () => {
1238
+ const form = useForm({
1239
+ initialData: {
1240
+ users: Array.from({ length: 1000 }, (_, i) => ({
1241
+ id: i,
1242
+ name: `User ${i}`,
1243
+ })),
1244
+ },
1245
+ })
1246
+
1247
+ // Create many subforms
1248
+ const subforms: Form<{
1249
+ id: number
1250
+ name: string
1251
+ }>[] = []
1252
+ for (let i = 0; i < 100; i++) {
1253
+ subforms.push(form.getSubForm(`users.${i}`))
1254
+ }
1255
+
1256
+ // Should not crash or cause memory issues
1257
+ expect(subforms).toHaveLength(100)
1258
+
1259
+ // Clear references
1260
+ subforms.length = 0
1261
+ })
1262
+
1263
+ it('should handle subform cleanup properly', () => {
1264
+ const form = useForm({
1265
+ initialData: {
1266
+ users: [
1267
+ { name: 'John' },
1268
+ { name: 'Jane' },
1269
+ ],
1270
+ },
1271
+ })
1272
+
1273
+ let userForm: Form<{
1274
+ name: string
1275
+ }> | undefined = form.getSubForm('users.0')
1276
+ expect(userForm.formData.value.name).toBe('John')
1277
+
1278
+ // Remove reference
1279
+ userForm = undefined
1280
+
1281
+ // Should not cause issues
1282
+ const newUserForm = form.getSubForm('users.0')
1283
+ expect(newUserForm.formData.value.name).toBe('John')
1284
+ })
1285
+ })
1286
+
1287
+ describe('Reactivity Performance', () => {
1288
+ it('should handle frequent data updates efficiently', async () => {
1289
+ const form = useForm({
1290
+ initialData: {
1291
+ user: { name: 'John' },
1292
+ },
1293
+ })
1294
+
1295
+ const userForm = form.getSubForm('user')
1296
+ const nameField = userForm.defineField({ path: 'name' })
1297
+
1298
+ const startTime = performance.now()
1299
+
1300
+ // Make many rapid updates
1301
+ for (let i = 0; i < 100; i++) {
1302
+ nameField.setValue(`Name ${i}`)
1303
+ await nextTick()
1304
+ }
1305
+
1306
+ const endTime = performance.now()
1307
+ const duration = endTime - startTime
1308
+
1309
+ // Should complete within reasonable time (less than 500ms)
1310
+ expect(duration).toBeLessThan(500)
1311
+ expect(nameField.value.value).toBe('Name 99')
1312
+ })
1313
+
1314
+ it('should handle nested reactivity updates efficiently', async () => {
1315
+ const form = useForm({
1316
+ initialData: {
1317
+ user: {
1318
+ profile: {
1319
+ personal: {
1320
+ name: 'John',
1321
+ },
1322
+ },
1323
+ },
1324
+ },
1325
+ })
1326
+
1327
+ const userForm = form.getSubForm('user')
1328
+ const profileForm = userForm.getSubForm('profile')
1329
+ const personalForm = profileForm.getSubForm('personal')
1330
+
1331
+ const startTime = performance.now()
1332
+
1333
+ // Make updates at different levels
1334
+ for (let i = 0; i < 50; i++) {
1335
+ personalForm.formData.value.name = `Name ${i}`
1336
+ await nextTick()
1337
+ }
1338
+
1339
+ const endTime = performance.now()
1340
+ const duration = endTime - startTime
1341
+
1342
+ // Should complete within reasonable time
1343
+ expect(duration).toBeLessThan(500)
1344
+ expect(personalForm.formData.value.name).toBe('Name 49')
1345
+ })
1346
+ })
1347
+ })
1348
+ })