@teamnovu/kit-vue-forms 0.1.21 → 0.1.22
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/composables/useFieldArray.d.ts +12 -2
- package/dist/composables/useSubform.d.ts +1 -1
- package/dist/index.js +411 -351
- package/dist/types/form.d.ts +16 -0
- package/dist/{composables/useSubmitHandler.d.ts → utils/submitHandler.d.ts} +1 -1
- package/package.json +1 -1
- package/src/composables/useFieldArray.ts +137 -8
- package/src/composables/useForm.ts +14 -20
- package/src/composables/useSubform.ts +66 -70
- package/src/types/form.ts +26 -0
- package/src/{composables/useSubmitHandler.ts → utils/submitHandler.ts} +2 -2
- package/tests/useFieldArray.test.ts +381 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
3
|
+
import { useForm } from '../src/composables/useForm'
|
|
4
|
+
import { HashStore, useFieldArray } from '../src/composables/useFieldArray'
|
|
5
|
+
|
|
6
|
+
describe('useFieldArray', () => {
|
|
7
|
+
describe('HashStore', () => {
|
|
8
|
+
it('should store and retrieve primitive values with identity hash', () => {
|
|
9
|
+
const store = new HashStore<string[]>()
|
|
10
|
+
|
|
11
|
+
store.set(5, ['id-1'])
|
|
12
|
+
expect(store.has(5)).toBe(true)
|
|
13
|
+
expect(store.get(5)).toEqual(['id-1'])
|
|
14
|
+
|
|
15
|
+
store.set(10, ['id-2'])
|
|
16
|
+
expect(store.get(10)).toEqual(['id-2'])
|
|
17
|
+
expect(store.get(5)).toEqual(['id-1'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should store objects by reference with identity hash', () => {
|
|
21
|
+
const store = new HashStore<string[]>()
|
|
22
|
+
const obj1 = {
|
|
23
|
+
id: 1,
|
|
24
|
+
name: 'A',
|
|
25
|
+
}
|
|
26
|
+
const obj2 = {
|
|
27
|
+
id: 1,
|
|
28
|
+
name: 'A',
|
|
29
|
+
} // Same content, different reference
|
|
30
|
+
|
|
31
|
+
store.set(obj1, ['uuid-1'])
|
|
32
|
+
|
|
33
|
+
expect(store.has(obj1)).toBe(true)
|
|
34
|
+
expect(store.has(obj2)).toBe(false)
|
|
35
|
+
expect(store.get(obj1)).toEqual(['uuid-1'])
|
|
36
|
+
expect(store.get(obj2)).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should use custom hash function for semantic equality', () => {
|
|
40
|
+
const store = new HashStore<string[], { id: number }>(item => item.id)
|
|
41
|
+
const obj1 = {
|
|
42
|
+
id: 1,
|
|
43
|
+
name: 'A',
|
|
44
|
+
}
|
|
45
|
+
const obj2 = {
|
|
46
|
+
id: 1,
|
|
47
|
+
name: 'B',
|
|
48
|
+
} // Same ID, different name
|
|
49
|
+
const obj3 = {
|
|
50
|
+
id: 2,
|
|
51
|
+
name: 'A',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
store.set(obj1, ['uuid-1'])
|
|
55
|
+
|
|
56
|
+
expect(store.has(obj2)).toBe(true)
|
|
57
|
+
expect(store.get(obj2)).toEqual(['uuid-1'])
|
|
58
|
+
expect(store.has(obj3)).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should overwrite value on subsequent set calls', () => {
|
|
62
|
+
const store = new HashStore<string[]>()
|
|
63
|
+
const item = { id: 1 }
|
|
64
|
+
|
|
65
|
+
store.set(item, ['id-1'])
|
|
66
|
+
expect(store.get(item)).toEqual(['id-1'])
|
|
67
|
+
|
|
68
|
+
store.set(item, ['id-1', 'id-2'])
|
|
69
|
+
expect(store.get(item)).toEqual(['id-1', 'id-2'])
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('Core Functionality', () => {
|
|
74
|
+
it('should initialize with array data and generate IDs', () => {
|
|
75
|
+
const form = useForm({
|
|
76
|
+
initialData: {
|
|
77
|
+
items: [
|
|
78
|
+
{
|
|
79
|
+
id: 1,
|
|
80
|
+
name: 'Item 1',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 2,
|
|
84
|
+
name: 'Item 2',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
const fieldArray = useFieldArray(form, 'items')
|
|
90
|
+
|
|
91
|
+
expect(fieldArray.fields.value).toHaveLength(2)
|
|
92
|
+
expect(fieldArray.fields.value[0]).toHaveProperty('id')
|
|
93
|
+
expect(fieldArray.fields.value[0]).toHaveProperty('item')
|
|
94
|
+
expect(fieldArray.fields.value[0].item).toEqual({
|
|
95
|
+
id: 1,
|
|
96
|
+
name: 'Item 1',
|
|
97
|
+
})
|
|
98
|
+
expect(typeof fieldArray.fields.value[0].id).toBe('string')
|
|
99
|
+
expect(fieldArray.errors.value).toEqual([])
|
|
100
|
+
expect(fieldArray.dirty.value).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should push new items to the array', async () => {
|
|
104
|
+
const form = useForm({
|
|
105
|
+
initialData: {
|
|
106
|
+
items: [
|
|
107
|
+
{
|
|
108
|
+
id: 1,
|
|
109
|
+
name: 'First',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
const fieldArray = useFieldArray(form, 'items')
|
|
115
|
+
|
|
116
|
+
expect(fieldArray.fields.value).toHaveLength(1)
|
|
117
|
+
|
|
118
|
+
fieldArray.push({
|
|
119
|
+
id: 2,
|
|
120
|
+
name: 'Second',
|
|
121
|
+
})
|
|
122
|
+
await nextTick()
|
|
123
|
+
|
|
124
|
+
expect(fieldArray.fields.value).toHaveLength(2)
|
|
125
|
+
expect(fieldArray.fields.value[1].item).toEqual({
|
|
126
|
+
id: 2,
|
|
127
|
+
name: 'Second',
|
|
128
|
+
})
|
|
129
|
+
expect(fieldArray.dirty.value).toBe(true)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should remove item by reference', async () => {
|
|
133
|
+
const form = useForm({
|
|
134
|
+
initialData: {
|
|
135
|
+
items: [
|
|
136
|
+
{
|
|
137
|
+
id: 1,
|
|
138
|
+
name: 'Keep',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 2,
|
|
142
|
+
name: 'Remove',
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
const fieldArray = useFieldArray(form, 'items')
|
|
148
|
+
const itemToRemove = fieldArray.fields.value[1].item
|
|
149
|
+
|
|
150
|
+
fieldArray.remove(itemToRemove)
|
|
151
|
+
await nextTick()
|
|
152
|
+
|
|
153
|
+
expect(fieldArray.fields.value).toHaveLength(1)
|
|
154
|
+
expect(fieldArray.fields.value[0].item.name).toBe('Keep')
|
|
155
|
+
expect(fieldArray.dirty.value).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should remove item by index', async () => {
|
|
159
|
+
const form = useForm({
|
|
160
|
+
initialData: {
|
|
161
|
+
items: [
|
|
162
|
+
{
|
|
163
|
+
id: 1,
|
|
164
|
+
name: 'First',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 2,
|
|
168
|
+
name: 'Second',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 3,
|
|
172
|
+
name: 'Third',
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
const fieldArray = useFieldArray(form, 'items')
|
|
178
|
+
|
|
179
|
+
fieldArray.removeByIndex(1)
|
|
180
|
+
await nextTick()
|
|
181
|
+
|
|
182
|
+
expect(fieldArray.fields.value).toHaveLength(2)
|
|
183
|
+
expect(fieldArray.fields.value[0].item.name).toBe('First')
|
|
184
|
+
expect(fieldArray.fields.value[1].item.name).toBe('Third')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('ID Persistence', () => {
|
|
189
|
+
it('should maintain IDs when items are reordered', async () => {
|
|
190
|
+
const form = useForm({
|
|
191
|
+
initialData: {
|
|
192
|
+
items: [
|
|
193
|
+
{
|
|
194
|
+
id: 1,
|
|
195
|
+
name: 'First',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 2,
|
|
199
|
+
name: 'Second',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 3,
|
|
203
|
+
name: 'Third',
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
const fieldArray = useFieldArray(form, 'items', {
|
|
209
|
+
hashFn: (item: { id: number }) => item.id,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Capture initial IDs
|
|
213
|
+
const id1 = fieldArray.fields.value[0].id
|
|
214
|
+
const id2 = fieldArray.fields.value[1].id
|
|
215
|
+
const id3 = fieldArray.fields.value[2].id
|
|
216
|
+
|
|
217
|
+
// Reverse the array
|
|
218
|
+
const arrayField = form.getField('items')
|
|
219
|
+
arrayField.setData([...arrayField.data.value].reverse())
|
|
220
|
+
await nextTick()
|
|
221
|
+
|
|
222
|
+
// Verify order changed but IDs followed the items
|
|
223
|
+
expect(fieldArray.fields.value[0].item.name).toBe('Third')
|
|
224
|
+
expect(fieldArray.fields.value[0].id).toBe(id3)
|
|
225
|
+
expect(fieldArray.fields.value[1].item.name).toBe('Second')
|
|
226
|
+
expect(fieldArray.fields.value[1].id).toBe(id2)
|
|
227
|
+
expect(fieldArray.fields.value[2].item.name).toBe('First')
|
|
228
|
+
expect(fieldArray.fields.value[2].id).toBe(id1)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should maintain IDs based on custom hash function', async () => {
|
|
232
|
+
const form = useForm({
|
|
233
|
+
initialData: {
|
|
234
|
+
products: [
|
|
235
|
+
{
|
|
236
|
+
sku: 'ABC',
|
|
237
|
+
name: 'Widget',
|
|
238
|
+
price: 10,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
sku: 'DEF',
|
|
242
|
+
name: 'Gadget',
|
|
243
|
+
price: 20,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
const fieldArray = useFieldArray(form, 'products', {
|
|
249
|
+
hashFn: (item: { sku: string }) => item.sku,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const idABC = fieldArray.fields.value[0].id
|
|
253
|
+
const idDEF = fieldArray.fields.value[1].id
|
|
254
|
+
|
|
255
|
+
// Update price and swap order
|
|
256
|
+
const arrayField = form.getField('products')
|
|
257
|
+
arrayField.setData([
|
|
258
|
+
{
|
|
259
|
+
sku: 'DEF',
|
|
260
|
+
name: 'Gadget',
|
|
261
|
+
price: 25,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
sku: 'ABC',
|
|
265
|
+
name: 'Widget',
|
|
266
|
+
price: 15,
|
|
267
|
+
},
|
|
268
|
+
])
|
|
269
|
+
await nextTick()
|
|
270
|
+
|
|
271
|
+
// IDs should persist because SKUs didn't change
|
|
272
|
+
expect(fieldArray.fields.value[0].id).toBe(idDEF)
|
|
273
|
+
expect(fieldArray.fields.value[1].id).toBe(idABC)
|
|
274
|
+
|
|
275
|
+
// Change SKU - should get new ID
|
|
276
|
+
arrayField.setData([
|
|
277
|
+
{
|
|
278
|
+
sku: 'XYZ',
|
|
279
|
+
name: 'Widget',
|
|
280
|
+
price: 15,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
sku: 'DEF',
|
|
284
|
+
name: 'Gadget',
|
|
285
|
+
price: 25,
|
|
286
|
+
},
|
|
287
|
+
])
|
|
288
|
+
await nextTick()
|
|
289
|
+
|
|
290
|
+
expect(fieldArray.fields.value[0].id).not.toBe(idABC)
|
|
291
|
+
expect(fieldArray.fields.value[1].id).toBe(idDEF)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should assign IDs in order for items with same hash', async () => {
|
|
295
|
+
// When multiple items share the same hash, IDs are assigned in order
|
|
296
|
+
// from the stored ID list. This means reordering items with same hash
|
|
297
|
+
// will NOT preserve ID-to-item mapping (IDs follow position, not identity).
|
|
298
|
+
const form = useForm({
|
|
299
|
+
initialData: {
|
|
300
|
+
items: [
|
|
301
|
+
{
|
|
302
|
+
type: 'tag',
|
|
303
|
+
value: 'vue',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
type: 'tag',
|
|
307
|
+
value: 'react',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: 'tag',
|
|
311
|
+
value: 'svelte',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
// Hash by type only - all items have same hash 'tag'
|
|
317
|
+
const fieldArray = useFieldArray(form, 'items', {
|
|
318
|
+
hashFn: (item: { type: string }) => item.type,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// All items get unique IDs despite same hash
|
|
322
|
+
const [id1, id2, id3] = fieldArray.fields.value.map(f => f.id)
|
|
323
|
+
expect(new Set([id1, id2, id3]).size).toBe(3)
|
|
324
|
+
|
|
325
|
+
// Reorder: move last item to first position
|
|
326
|
+
const arrayField = form.getField('items')
|
|
327
|
+
arrayField.setData([
|
|
328
|
+
{
|
|
329
|
+
type: 'tag',
|
|
330
|
+
value: 'svelte',
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
type: 'tag',
|
|
334
|
+
value: 'vue',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
type: 'tag',
|
|
338
|
+
value: 'react',
|
|
339
|
+
},
|
|
340
|
+
])
|
|
341
|
+
await nextTick()
|
|
342
|
+
|
|
343
|
+
// IDs are assigned in order from stored list, NOT following items
|
|
344
|
+
// This is expected behavior with hash collisions
|
|
345
|
+
expect(fieldArray.fields.value[0].id).toBe(id1) // svelte gets id1 (was vue's)
|
|
346
|
+
expect(fieldArray.fields.value[1].id).toBe(id2) // vue gets id2 (was react's)
|
|
347
|
+
expect(fieldArray.fields.value[2].id).toBe(id3) // react gets id3 (was svelte's)
|
|
348
|
+
|
|
349
|
+
// The values confirm the reorder happened
|
|
350
|
+
expect(fieldArray.fields.value[0].item.value).toBe('svelte')
|
|
351
|
+
expect(fieldArray.fields.value[1].item.value).toBe('vue')
|
|
352
|
+
expect(fieldArray.fields.value[2].item.value).toBe('react')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('Form Integration', () => {
|
|
357
|
+
it('should track dirty state correctly', async () => {
|
|
358
|
+
const form = useForm({
|
|
359
|
+
initialData: { items: [{ name: 'Item' }] },
|
|
360
|
+
})
|
|
361
|
+
const fieldArray = useFieldArray(form, 'items')
|
|
362
|
+
|
|
363
|
+
expect(form.isDirty.value).toBe(false)
|
|
364
|
+
expect(fieldArray.dirty.value).toBe(false)
|
|
365
|
+
|
|
366
|
+
// Make a change
|
|
367
|
+
fieldArray.push({ name: 'New Item' })
|
|
368
|
+
await nextTick()
|
|
369
|
+
|
|
370
|
+
expect(form.isDirty.value).toBe(true)
|
|
371
|
+
expect(fieldArray.dirty.value).toBe(true)
|
|
372
|
+
|
|
373
|
+
// Reset form
|
|
374
|
+
form.reset()
|
|
375
|
+
await nextTick()
|
|
376
|
+
|
|
377
|
+
expect(form.isDirty.value).toBe(false)
|
|
378
|
+
expect(fieldArray.dirty.value).toBe(false)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
})
|