@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,651 @@
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
+
6
+ describe('Nested Path Handling', () => {
7
+ interface TestFormData {
8
+ user: {
9
+ name: string
10
+ email: string
11
+ profile: {
12
+ age: number
13
+ bio: string
14
+ preferences: {
15
+ theme: string
16
+ notifications: boolean
17
+ }
18
+ }
19
+ }
20
+ company: {
21
+ name: string
22
+ address: {
23
+ street: string
24
+ city: string
25
+ country: string
26
+ coordinates: {
27
+ lat: number
28
+ lng: number
29
+ }
30
+ }
31
+ }
32
+ tags: string[]
33
+ contacts: Array<{
34
+ id: string
35
+ name: string
36
+ email: string
37
+ addresses: Array<{
38
+ type: string
39
+ street: string
40
+ city: string
41
+ }>
42
+ }>
43
+ metadata: Record<string, any>
44
+ }
45
+
46
+ const initialData: TestFormData = {
47
+ user: {
48
+ name: 'John Doe',
49
+ email: 'john@example.com',
50
+ profile: {
51
+ age: 30,
52
+ bio: 'Software developer',
53
+ preferences: {
54
+ theme: 'dark',
55
+ notifications: true,
56
+ },
57
+ },
58
+ },
59
+ company: {
60
+ name: 'Tech Corp',
61
+ address: {
62
+ street: '123 Main St',
63
+ city: 'New York',
64
+ country: 'USA',
65
+ coordinates: {
66
+ lat: 40.7128,
67
+ lng: -74.0060,
68
+ },
69
+ },
70
+ },
71
+ tags: ['javascript', 'vue', 'typescript'],
72
+ contacts: [
73
+ {
74
+ id: '1',
75
+ name: 'Jane Smith',
76
+ email: 'jane@example.com',
77
+ addresses: [
78
+ {
79
+ type: 'home',
80
+ street: '456 Oak Ave',
81
+ city: 'Boston',
82
+ },
83
+ {
84
+ type: 'work',
85
+ street: '789 Pine St',
86
+ city: 'Cambridge',
87
+ },
88
+ ],
89
+ },
90
+ {
91
+ id: '2',
92
+ name: 'Bob Johnson',
93
+ email: 'bob@example.com',
94
+ addresses: [
95
+ {
96
+ type: 'home',
97
+ street: '321 Elm St',
98
+ city: 'Seattle',
99
+ },
100
+ ],
101
+ },
102
+ ],
103
+ metadata: {
104
+ version: '1.0',
105
+ created: '2023-01-01',
106
+ settings: {
107
+ debug: false,
108
+ timeout: 5000,
109
+ },
110
+ },
111
+ }
112
+
113
+ describe('Basic Nested Path Field Operations', () => {
114
+ it('should define and access fields with simple nested paths', () => {
115
+ const form = useForm({ initialData })
116
+
117
+ const nameField = form.defineField({ path: 'user.name' })
118
+ const emailField = form.defineField({ path: 'user.email' })
119
+
120
+ expect(nameField.value.value).toBe('John Doe')
121
+ expect(emailField.value.value).toBe('john@example.com')
122
+ })
123
+
124
+ it('should define and access fields with deeply nested paths', () => {
125
+ const form = useForm({ initialData })
126
+
127
+ const ageField = form.defineField({ path: 'user.profile.age' })
128
+ const themeField = form.defineField({ path: 'user.profile.preferences.theme' })
129
+ const notificationsField = form.defineField({ path: 'user.profile.preferences.notifications' })
130
+
131
+ expect(ageField.value.value).toBe(30)
132
+ expect(themeField.value.value).toBe('dark')
133
+ expect(notificationsField.value.value).toBe(true)
134
+ })
135
+
136
+ it('should update nested field values reactively', async () => {
137
+ const form = useForm({ initialData })
138
+
139
+ const nameField = form.defineField({ path: 'user.name' })
140
+ const ageField = form.defineField({ path: 'user.profile.age' })
141
+
142
+ nameField.value.value = 'Jane Doe'
143
+ ageField.value.value = 25
144
+
145
+ await nextTick()
146
+
147
+ expect(form.formData.value.user.name).toBe('Jane Doe')
148
+ expect(form.formData.value.user.profile.age).toBe(25)
149
+ })
150
+
151
+ it('should retrieve fields using getField with nested paths', () => {
152
+ const form = useForm({ initialData })
153
+
154
+ form.defineField({ path: 'user.name' })
155
+ form.defineField({ path: 'user.profile.age' })
156
+
157
+ const nameField = form.getField('user.name')
158
+ const ageField = form.getField('user.profile.age')
159
+
160
+ expect(nameField).toBeDefined()
161
+ expect(ageField).toBeDefined()
162
+ expect(nameField?.value.value).toBe('John Doe')
163
+ expect(ageField?.value.value).toBe(30)
164
+ })
165
+
166
+ it('should handle nested paths with complex coordinate data', () => {
167
+ const form = useForm({ initialData })
168
+
169
+ const latField = form.defineField({ path: 'company.address.coordinates.lat' })
170
+ const lngField = form.defineField({ path: 'company.address.coordinates.lng' })
171
+
172
+ expect(latField.value.value).toBe(40.7128)
173
+ expect(lngField.value.value).toBe(-74.0060)
174
+
175
+ latField.value.value = 41.8781
176
+ lngField.value.value = -87.6298
177
+
178
+ expect(form.formData.value.company.address.coordinates.lat).toBe(41.8781)
179
+ expect(form.formData.value.company.address.coordinates.lng).toBe(-87.6298)
180
+ })
181
+ })
182
+
183
+ describe('Array Path Handling', () => {
184
+ it('should handle array index paths', () => {
185
+ const form = useForm({ initialData })
186
+
187
+ const firstTagField = form.defineField({ path: 'tags.0' })
188
+ const secondTagField = form.defineField({ path: 'tags.1' })
189
+
190
+ expect(firstTagField.value.value).toBe('javascript')
191
+ expect(secondTagField.value.value).toBe('vue')
192
+ })
193
+
194
+ it('should handle nested object arrays with index paths', () => {
195
+ const form = useForm({ initialData })
196
+
197
+ const firstContactNameField = form.defineField({ path: 'contacts.0.name' })
198
+ const firstContactEmailField = form.defineField({ path: 'contacts.0.email' })
199
+ const secondContactNameField = form.defineField({ path: 'contacts.1.name' })
200
+
201
+ expect(firstContactNameField.value.value).toBe('Jane Smith')
202
+ expect(firstContactEmailField.value.value).toBe('jane@example.com')
203
+ expect(secondContactNameField.value.value).toBe('Bob Johnson')
204
+ })
205
+
206
+ it('should handle deeply nested arrays with multiple levels', () => {
207
+ const form = useForm({ initialData })
208
+
209
+ const firstContactFirstAddressTypeField = form.defineField({
210
+ path: 'contacts.0.addresses.0.type',
211
+ })
212
+ const firstContactFirstAddressStreetField = form.defineField({
213
+ path: 'contacts.0.addresses.0.street',
214
+ })
215
+ const firstContactSecondAddressTypeField = form.defineField({
216
+ path: 'contacts.0.addresses.1.type',
217
+ })
218
+ const secondContactFirstAddressStreetField = form.defineField({
219
+ path: 'contacts.1.addresses.0.street',
220
+ })
221
+
222
+ expect(firstContactFirstAddressTypeField.value.value).toBe('home')
223
+ expect(firstContactFirstAddressStreetField.value.value).toBe('456 Oak Ave')
224
+ expect(firstContactSecondAddressTypeField.value.value).toBe('work')
225
+ expect(secondContactFirstAddressStreetField.value.value).toBe('321 Elm St')
226
+ })
227
+
228
+ it('should update array element values reactively', async () => {
229
+ const form = useForm({ initialData })
230
+
231
+ const firstTagField = form.defineField({ path: 'tags.0' })
232
+ const firstContactNameField = form.defineField({ path: 'contacts.0.name' })
233
+
234
+ firstTagField.value.value = 'react'
235
+ firstContactNameField.value.value = 'Janet Smith'
236
+
237
+ await nextTick()
238
+
239
+ expect(form.formData.value.tags[0]).toBe('react')
240
+ expect(form.formData.value.contacts[0].name).toBe('Janet Smith')
241
+ })
242
+
243
+ it('should handle dynamic array indices', async () => {
244
+ const form = useForm({ initialData })
245
+
246
+ // Test that we can access different indices
247
+ for (let i = 0; i < form.formData.value.tags.length; i++) {
248
+ const tagField = form.defineField({ path: `tags.${i}` as `tags.${number}` })
249
+ expect(tagField.value.value).toBe(initialData.tags[i])
250
+ }
251
+
252
+ // Test that we can access different contact indices
253
+ for (let i = 0; i < form.formData.value.contacts.length; i++) {
254
+ const contactNameField = form.defineField({ path: `contacts.${i}.name` as `contacts.${number}.name` })
255
+ expect(contactNameField.value.value).toBe(initialData.contacts[i].name)
256
+ }
257
+ })
258
+ })
259
+
260
+ describe('Dynamic and Record Object Paths', () => {
261
+ it('should handle record object paths', () => {
262
+ const form = useForm({ initialData })
263
+
264
+ const versionField = form.defineField({ path: 'metadata.version' })
265
+ const createdField = form.defineField({ path: 'metadata.created' })
266
+ const debugField = form.defineField({ path: 'metadata.settings.debug' })
267
+ const timeoutField = form.defineField({ path: 'metadata.settings.timeout' })
268
+
269
+ expect(versionField.value.value).toBe('1.0')
270
+ expect(createdField.value.value).toBe('2023-01-01')
271
+ expect(debugField.value.value).toBe(false)
272
+ expect(timeoutField.value.value).toBe(5000)
273
+ })
274
+
275
+ it('should handle record object updates', async () => {
276
+ const form = useForm({ initialData })
277
+
278
+ const versionField = form.defineField({ path: 'metadata.version' })
279
+ const debugField = form.defineField({ path: 'metadata.settings.debug' })
280
+
281
+ versionField.value.value = '2.0'
282
+ debugField.value.value = true
283
+
284
+ await nextTick()
285
+
286
+ expect(form.formData.value.metadata.version).toBe('2.0')
287
+ expect(form.formData.value.metadata.settings.debug).toBe(true)
288
+ })
289
+ })
290
+
291
+ describe('Field State Management with Nested Paths', () => {
292
+ it('should track dirty state for nested fields', async () => {
293
+ const form = useForm({ initialData })
294
+
295
+ const nameField = form.defineField({ path: 'user.name' })
296
+ const ageField = form.defineField({ path: 'user.profile.age' })
297
+
298
+ expect(nameField.dirty.value).toBe(false)
299
+ expect(ageField.dirty.value).toBe(false)
300
+
301
+ nameField.value.value = 'Jane Doe'
302
+ await nextTick()
303
+
304
+ expect(nameField.dirty.value).toBe(true)
305
+ expect(ageField.dirty.value).toBe(false)
306
+
307
+ ageField.value.value = 25
308
+ await nextTick()
309
+
310
+ expect(ageField.dirty.value).toBe(true)
311
+ })
312
+
313
+ it('should track touched state for nested fields', async () => {
314
+ const form = useForm({ initialData })
315
+
316
+ const nameField = form.defineField({ path: 'user.name' })
317
+ const emailField = form.defineField({ path: 'user.email' })
318
+
319
+ expect(nameField.touched.value).toBe(false)
320
+ expect(emailField.touched.value).toBe(false)
321
+
322
+ nameField.onBlur()
323
+ await nextTick()
324
+
325
+ expect(nameField.touched.value).toBe(true)
326
+ expect(emailField.touched.value).toBe(false)
327
+ })
328
+
329
+ it('should handle reset for nested fields', async () => {
330
+ const form = useForm({ initialData })
331
+
332
+ const nameField = form.defineField({ path: 'user.name' })
333
+ const ageField = form.defineField({ path: 'user.profile.age' })
334
+
335
+ nameField.value.value = 'Jane Doe'
336
+ ageField.value.value = 25
337
+ nameField.onBlur()
338
+
339
+ await nextTick()
340
+
341
+ expect(nameField.value.value).toBe('Jane Doe')
342
+ expect(nameField.dirty.value).toBe(true)
343
+ expect(nameField.touched.value).toBe(true)
344
+
345
+ nameField.reset()
346
+ await nextTick()
347
+
348
+ expect(nameField.value.value).toBe('John Doe')
349
+ expect(nameField.dirty.value).toBe(false)
350
+ expect(nameField.touched.value).toBe(false)
351
+ })
352
+ })
353
+
354
+ describe('Validation with Nested Paths', () => {
355
+ const schema = z.looseObject({
356
+ user: z.object({
357
+ name: z.string().min(2, 'Name must be at least 2 characters'),
358
+ email: z.string().email('Invalid email format'),
359
+ profile: z.object({
360
+ age: z.number().min(18, 'Must be at least 18'),
361
+ bio: z.string().min(10, 'Bio must be at least 10 characters'),
362
+ preferences: z.object({
363
+ theme: z.enum(['light', 'dark'], { message: 'Theme must be light or dark' }),
364
+ notifications: z.boolean(),
365
+ }),
366
+ }),
367
+ }),
368
+ company: z.object({
369
+ name: z.string().min(1, 'Company name is required'),
370
+ address: z.object({
371
+ street: z.string().min(1, 'Street is required'),
372
+ city: z.string().min(1, 'City is required'),
373
+ country: z.string().min(1, 'Country is required'),
374
+ coordinates: z.object({
375
+ lat: z.number().min(-90)
376
+ .max(90, 'Invalid latitude'),
377
+ lng: z.number().min(-180)
378
+ .max(180, 'Invalid longitude'),
379
+ }),
380
+ }),
381
+ }),
382
+ tags: z.array(z.string().min(1)),
383
+ contacts: z.array(z.object({
384
+ id: z.string(),
385
+ name: z.string().min(1, 'Contact name is required'),
386
+ email: z.string().email('Invalid contact email'),
387
+ addresses: z.array(z.object({
388
+ type: z.enum(['home', 'work'], { message: 'Address type must be home or work' }),
389
+ street: z.string().min(1, 'Address street is required'),
390
+ city: z.string().min(1, 'Address city is required'),
391
+ })),
392
+ })),
393
+ })
394
+
395
+ it('should validate nested field paths', async () => {
396
+ const form = useForm({
397
+ initialData,
398
+ schema,
399
+ })
400
+
401
+ const nameField = form.defineField({ path: 'user.name' })
402
+ const emailField = form.defineField({ path: 'user.email' })
403
+
404
+ nameField.value.value = 'A'
405
+ emailField.value.value = 'invalid-email'
406
+
407
+ await nextTick()
408
+
409
+ const result = await form.validateForm()
410
+
411
+ expect(result.isValid).toBe(false)
412
+ expect(result.errors.propertyErrors['user.name']).toContain('Name must be at least 2 characters')
413
+ expect(result.errors.propertyErrors['user.email']).toContain('Invalid email format')
414
+ })
415
+
416
+ it('should validate deeply nested paths', async () => {
417
+ const form = useForm({
418
+ initialData,
419
+ schema,
420
+ })
421
+
422
+ const ageField = form.defineField({ path: 'user.profile.age' })
423
+ const themeField = form.defineField({ path: 'user.profile.preferences.theme' })
424
+
425
+ ageField.value.value = 16
426
+ themeField.value.value = 'blue' as typeof themeField.value.value
427
+
428
+ await nextTick()
429
+
430
+ const result = await form.validateForm()
431
+
432
+ expect(result.isValid).toBe(false)
433
+ expect(result.errors.propertyErrors['user.profile.age']).toContain('Must be at least 18')
434
+ expect(result.errors.propertyErrors['user.profile.preferences.theme']).toContain('Theme must be light or dark')
435
+ })
436
+
437
+ it('should validate array element paths', async () => {
438
+ const form = useForm({
439
+ initialData,
440
+ schema,
441
+ })
442
+
443
+ const firstContactNameField = form.defineField({ path: 'contacts.0.name' })
444
+ const firstContactEmailField = form.defineField({ path: 'contacts.0.email' })
445
+
446
+ firstContactNameField.value.value = ''
447
+ firstContactEmailField.value.value = 'invalid-email'
448
+
449
+ await nextTick()
450
+
451
+ const result = await form.validateForm()
452
+
453
+ expect(result.isValid).toBe(false)
454
+ expect(result.errors.propertyErrors['contacts.0.name']).toContain('Contact name is required')
455
+ expect(result.errors.propertyErrors['contacts.0.email']).toContain('Invalid contact email')
456
+ })
457
+
458
+ it('should validate nested array paths', async () => {
459
+ const form = useForm({
460
+ initialData,
461
+ schema,
462
+ })
463
+
464
+ const addressTypeField = form.defineField({ path: 'contacts.0.addresses.0.type' })
465
+ const addressStreetField = form.defineField({ path: 'contacts.0.addresses.0.street' })
466
+
467
+ addressTypeField.value.value = 'office' as typeof addressTypeField.value.value
468
+ addressStreetField.value.value = ''
469
+
470
+ await nextTick()
471
+
472
+ const result = await form.validateForm()
473
+
474
+ expect(result.isValid).toBe(false)
475
+ expect(result.errors.propertyErrors['contacts.0.addresses.0.type']).toContain('Address type must be home or work')
476
+ expect(result.errors.propertyErrors['contacts.0.addresses.0.street']).toContain('Address street is required')
477
+ })
478
+ })
479
+
480
+ describe('Edge Cases and Error Handling', () => {
481
+ it('should handle non-existent nested paths gracefully', () => {
482
+ const form = useForm({ initialData })
483
+
484
+ const nonExistentField = form.defineField({ path: 'user.nonexistent' as any })
485
+
486
+ expect(nonExistentField.value.value).toBeUndefined()
487
+ })
488
+
489
+ it('should handle deeply non-existent paths', () => {
490
+ const form = useForm({ initialData })
491
+
492
+ const deepNonExistentField = form.defineField({ path: 'user.nonexistent.deep.path' as any })
493
+
494
+ expect(deepNonExistentField.value.value).toBeUndefined()
495
+ })
496
+
497
+ it('should handle array index out of bounds', () => {
498
+ const form = useForm({ initialData })
499
+
500
+ const outOfBoundsField = form.defineField({ path: 'tags.99' as any })
501
+
502
+ expect(outOfBoundsField.value.value).toBeUndefined()
503
+ })
504
+
505
+ it('should handle nested array index out of bounds', () => {
506
+ const form = useForm({ initialData })
507
+
508
+ const outOfBoundsField = form.defineField({ path: 'contacts.99.name' as any })
509
+
510
+ expect(outOfBoundsField.value.value).toBeUndefined()
511
+ })
512
+
513
+ it('should handle paths with spaces around dots', () => {
514
+ const form = useForm({ initialData })
515
+
516
+ const nameField = form.defineField({ path: 'user . name' as any })
517
+
518
+ expect(nameField.value.value).toBe('John Doe')
519
+ })
520
+
521
+ it('should handle empty path segments', () => {
522
+ const form = useForm({ initialData })
523
+
524
+ const nameField = form.defineField({ path: 'user..name' as any })
525
+
526
+ expect(nameField.value.value).toBe('John Doe')
527
+ })
528
+
529
+ it('should handle setting values on non-existent paths', async () => {
530
+ const form = useForm({ initialData })
531
+
532
+ const nonExistentField = form.defineField({ path: 'user.newField' as any })
533
+
534
+ nonExistentField.value.value = 'new value'
535
+
536
+ await nextTick()
537
+
538
+ expect((form.formData.value.user as any).newField).toBe('new value')
539
+ })
540
+ })
541
+
542
+ describe('Performance and Memory', () => {
543
+ it('should replace field when defining same path twice', () => {
544
+ const form = useForm({ initialData })
545
+
546
+ const field1 = form.defineField({ path: 'user.name' })
547
+ const field2 = form.defineField({ path: 'user.name' })
548
+
549
+ // Fields are not the same object, but second field replaces the first
550
+ expect(field1).not.toBe(field2)
551
+ expect(form.getField('user.name')).toBe(field2)
552
+ expect(form.getFields().length).toBe(1)
553
+ })
554
+
555
+ it('should handle large numbers of nested fields', () => {
556
+ const form = useForm({ initialData })
557
+
558
+ const fields = []
559
+
560
+ // Create many nested fields
561
+ for (let i = 0; i < 100; i++) {
562
+ if (i < form.formData.value.contacts.length) {
563
+ fields.push(form.defineField({ path: `contacts.${i}.name` }))
564
+ fields.push(form.defineField({ path: `contacts.${i}.email` }))
565
+ }
566
+ }
567
+
568
+ expect(fields.length).toBeGreaterThan(0)
569
+ expect(form.getFields().length).toBeGreaterThan(0)
570
+ })
571
+ })
572
+
573
+ describe('Integration with Form State', () => {
574
+ it('should properly integrate nested fields with form dirty state', async () => {
575
+ const form = useForm({ initialData })
576
+
577
+ const nameField = form.defineField({ path: 'user.name' })
578
+ form.defineField({ path: 'user.profile.age' })
579
+
580
+ expect(form.isDirty.value).toBe(false)
581
+
582
+ nameField.value.value = 'Jane Doe'
583
+ await nextTick()
584
+
585
+ expect(form.isDirty.value).toBe(true)
586
+
587
+ nameField.reset()
588
+ await nextTick()
589
+
590
+ expect(form.isDirty.value).toBe(false)
591
+ })
592
+
593
+ it('should properly integrate nested fields with form touched state', async () => {
594
+ const form = useForm({ initialData })
595
+
596
+ const nameField = form.defineField({ path: 'user.name' })
597
+ form.defineField({ path: 'user.email' })
598
+
599
+ expect(form.isTouched.value).toBe(false)
600
+
601
+ nameField.onBlur()
602
+ await nextTick()
603
+
604
+ expect(form.isTouched.value).toBe(true)
605
+
606
+ form.reset()
607
+ await nextTick()
608
+
609
+ expect(form.isTouched.value).toBe(false)
610
+ })
611
+
612
+ it('should properly integrate nested fields with form validation state', async () => {
613
+ const schema = z.object({
614
+ user: z.object({
615
+ name: z.string().min(2),
616
+ email: z.string().email(),
617
+ }),
618
+ })
619
+
620
+ const form = useForm({
621
+ initialData,
622
+ schema,
623
+ })
624
+
625
+ const nameField = form.defineField({ path: 'user.name' })
626
+ const emailField = form.defineField({ path: 'user.email' })
627
+
628
+ // Initial validation
629
+ await form.validateForm()
630
+ expect(form.isValid.value).toBe(true)
631
+
632
+ nameField.value.value = 'A'
633
+ emailField.value.value = 'invalid'
634
+
635
+ await nextTick()
636
+
637
+ // Trigger validation after changes
638
+ await form.validateForm()
639
+ expect(form.isValid.value).toBe(false)
640
+
641
+ nameField.value.value = 'John Doe'
642
+ emailField.value.value = 'john@example.com'
643
+
644
+ await nextTick()
645
+
646
+ // Trigger validation after changes
647
+ await form.validateForm()
648
+ expect(form.isValid.value).toBe(true)
649
+ })
650
+ })
651
+ })