@teamnovu/kit-vue-forms 0.2.18 → 0.3.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/CHANGELOG.md +16 -0
- package/dist/components/Field.vue.d.ts +4 -1
- package/dist/composables/useFieldRegistry.d.ts +2 -1
- package/dist/composables/useInitialDataOverride.d.ts +13 -0
- package/dist/index.js +489 -439
- package/dist/types/form.d.ts +15 -1
- package/dist/utils/path.d.ts +1 -0
- package/docs/reference.md +65 -0
- package/package.json +1 -1
- package/src/composables/useField.ts +10 -19
- package/src/composables/useFieldArray.ts +16 -0
- package/src/composables/useFieldRegistry.ts +33 -10
- package/src/composables/useForm.ts +4 -2
- package/src/composables/useInitialDataOverride.ts +130 -0
- package/src/types/form.ts +18 -1
- package/src/utils/path.ts +11 -0
- package/tests/formState.test.ts +6 -3
- package/tests/initialDataOverride.test.ts +479 -0
- package/tests/useFieldArray.test.ts +112 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useForm } from '../src/composables/useForm'
|
|
4
|
+
|
|
5
|
+
describe('initialData overrides — subfield propagation', () => {
|
|
6
|
+
it('default merge: subfields see ancestor override and stay clean', () => {
|
|
7
|
+
const form = useForm({
|
|
8
|
+
initialData: {
|
|
9
|
+
user: {
|
|
10
|
+
name: 'A',
|
|
11
|
+
email: 'x@x',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const userField = form.getField('user')
|
|
17
|
+
const nameField = form.getField('user.name')
|
|
18
|
+
const emailField = form.getField('user.email')
|
|
19
|
+
|
|
20
|
+
userField.setInitialData({
|
|
21
|
+
name: 'B',
|
|
22
|
+
email: 'x@x',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
expect(nameField.initialValue.value).toBe('B')
|
|
26
|
+
expect(nameField.dirty.value).toBe(false)
|
|
27
|
+
|
|
28
|
+
// email was not touched by the override → falls through to external
|
|
29
|
+
expect(emailField.initialValue.value).toBe('x@x')
|
|
30
|
+
expect(emailField.dirty.value).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('partial merge at parent path leaves untouched siblings on external value', () => {
|
|
34
|
+
const form = useForm({
|
|
35
|
+
initialData: {
|
|
36
|
+
user: {
|
|
37
|
+
name: 'A',
|
|
38
|
+
email: 'x@x',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const userField = form.getField('user')
|
|
44
|
+
const emailField = form.getField('user.email')
|
|
45
|
+
|
|
46
|
+
// Override only `name` — `email` should fall through to the external value
|
|
47
|
+
userField.setInitialData({ name: 'B' } as {
|
|
48
|
+
name: string
|
|
49
|
+
email: string
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(form.getField('user.name').initialValue.value).toBe('B')
|
|
53
|
+
expect(emailField.initialValue.value).toBe('x@x')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('replace flag drops sibling keys at the overridden path', () => {
|
|
57
|
+
const form = useForm({
|
|
58
|
+
initialData: {
|
|
59
|
+
user: {
|
|
60
|
+
name: 'A',
|
|
61
|
+
email: 'x@x',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const userField = form.getField('user')
|
|
67
|
+
const emailField = form.getField('user.email')
|
|
68
|
+
|
|
69
|
+
userField.setInitialData(
|
|
70
|
+
{ name: 'B' } as {
|
|
71
|
+
name: string
|
|
72
|
+
email: string
|
|
73
|
+
},
|
|
74
|
+
{ replace: true },
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expect(form.getField('user.name').initialValue.value).toBe('B')
|
|
78
|
+
expect(emailField.initialValue.value).toBeUndefined()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('external initialData reassignment wipes overrides', () => {
|
|
82
|
+
const initialData = ref({
|
|
83
|
+
user: {
|
|
84
|
+
name: 'A',
|
|
85
|
+
email: 'x@x',
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const form = useForm({ initialData })
|
|
90
|
+
const userField = form.getField('user')
|
|
91
|
+
const nameField = form.getField('user.name')
|
|
92
|
+
|
|
93
|
+
userField.setInitialData({
|
|
94
|
+
name: 'B',
|
|
95
|
+
email: 'x@x',
|
|
96
|
+
})
|
|
97
|
+
expect(nameField.initialValue.value).toBe('B')
|
|
98
|
+
|
|
99
|
+
initialData.value = {
|
|
100
|
+
user: {
|
|
101
|
+
name: 'C',
|
|
102
|
+
email: 'y@y',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
expect(nameField.initialValue.value).toBe('C')
|
|
107
|
+
expect(form.getField('user.email').initialValue.value).toBe('y@y')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('overriding an ancestor drops a descendant override (cascade)', () => {
|
|
111
|
+
const form = useForm({
|
|
112
|
+
initialData: {
|
|
113
|
+
user: {
|
|
114
|
+
name: 'A',
|
|
115
|
+
email: 'x@x',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const userField = form.getField('user')
|
|
121
|
+
const nameField = form.getField('user.name')
|
|
122
|
+
|
|
123
|
+
nameField.setInitialData('X')
|
|
124
|
+
expect(nameField.initialValue.value).toBe('X')
|
|
125
|
+
|
|
126
|
+
// Writing to the ancestor invalidates the descendant override
|
|
127
|
+
userField.setInitialData({
|
|
128
|
+
name: 'B',
|
|
129
|
+
email: 'x@x',
|
|
130
|
+
})
|
|
131
|
+
expect(nameField.initialValue.value).toBe('B')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('sibling-prefix paths are not dropped by cascade', () => {
|
|
135
|
+
type Data = {
|
|
136
|
+
users1: { name: string }
|
|
137
|
+
users10: { name: string }
|
|
138
|
+
}
|
|
139
|
+
const form = useForm<Data>({
|
|
140
|
+
initialData: {
|
|
141
|
+
users1: { name: 'one' },
|
|
142
|
+
users10: { name: 'ten' },
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const users1Field = form.getField('users1')
|
|
147
|
+
const users10Field = form.getField('users10')
|
|
148
|
+
|
|
149
|
+
users1Field.setInitialData({ name: 'one-override' })
|
|
150
|
+
expect(form.getField('users1.name').initialValue.value).toBe('one-override')
|
|
151
|
+
|
|
152
|
+
// Writing to a sibling whose path is a prefix-collision string must not
|
|
153
|
+
// touch the existing override on `users1`.
|
|
154
|
+
users10Field.setInitialData({ name: 'ten-override' })
|
|
155
|
+
|
|
156
|
+
expect(form.getField('users1.name').initialValue.value).toBe('one-override')
|
|
157
|
+
expect(form.getField('users10.name').initialValue.value).toBe('ten-override')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('clean parent override pushes value into subfield data', () => {
|
|
161
|
+
const form = useForm({
|
|
162
|
+
initialData: {
|
|
163
|
+
user: {
|
|
164
|
+
name: 'A',
|
|
165
|
+
email: 'x@x',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const userField = form.getField('user')
|
|
171
|
+
const nameField = form.getField('user.name')
|
|
172
|
+
|
|
173
|
+
userField.setInitialData({
|
|
174
|
+
name: 'B',
|
|
175
|
+
email: 'x@x',
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
expect(nameField.data.value).toBe('B')
|
|
179
|
+
expect(form.isDirty.value).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('dirty subfield keeps its value when an ancestor override is set', () => {
|
|
183
|
+
const form = useForm({
|
|
184
|
+
initialData: {
|
|
185
|
+
user: {
|
|
186
|
+
name: 'A',
|
|
187
|
+
email: 'x@x',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const userField = form.getField('user')
|
|
193
|
+
const nameField = form.getField('user.name')
|
|
194
|
+
|
|
195
|
+
nameField.setData('Z')
|
|
196
|
+
expect(nameField.dirty.value).toBe(true)
|
|
197
|
+
|
|
198
|
+
userField.setInitialData({
|
|
199
|
+
name: 'B',
|
|
200
|
+
email: 'x@x',
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Field is dirty → data is preserved, but baseline still tracks the
|
|
204
|
+
// override (so dirty stays true because Z ≠ B).
|
|
205
|
+
expect(nameField.data.value).toBe('Z')
|
|
206
|
+
expect(nameField.initialValue.value).toBe('B')
|
|
207
|
+
expect(nameField.dirty.value).toBe(true)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('subtree scope: strict ancestor reads external baseline, override path and below read override', () => {
|
|
211
|
+
const form = useForm({
|
|
212
|
+
initialData: {
|
|
213
|
+
outer: {
|
|
214
|
+
inner: {
|
|
215
|
+
name: 'A',
|
|
216
|
+
email: 'x@x',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const outerField = form.getField('outer')
|
|
223
|
+
const innerField = form.getField('outer.inner')
|
|
224
|
+
const nameField = form.getField('outer.inner.name')
|
|
225
|
+
|
|
226
|
+
innerField.setInitialData(
|
|
227
|
+
{
|
|
228
|
+
name: 'B',
|
|
229
|
+
email: 'x@x',
|
|
230
|
+
},
|
|
231
|
+
{ scope: 'subtree' },
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Override path and descendants see the override.
|
|
235
|
+
expect(innerField.initialValue.value).toEqual({
|
|
236
|
+
name: 'B',
|
|
237
|
+
email: 'x@x',
|
|
238
|
+
})
|
|
239
|
+
expect(nameField.initialValue.value).toBe('B')
|
|
240
|
+
expect(nameField.dirty.value).toBe(false)
|
|
241
|
+
|
|
242
|
+
// Strict ancestor (outer) keeps the external baseline. Data was pushed
|
|
243
|
+
// into the inner subtree (the inner field was clean), so outer.data now
|
|
244
|
+
// diverges from outer's external initial → dirty.
|
|
245
|
+
expect(outerField.initialValue.value).toEqual({
|
|
246
|
+
inner: {
|
|
247
|
+
name: 'A',
|
|
248
|
+
email: 'x@x',
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
expect(outerField.dirty.value).toBe(true)
|
|
252
|
+
expect(form.isDirty.value).toBe(true)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('subtree scope on array index: array stays dirty, item subfields clean', () => {
|
|
256
|
+
type Row = {
|
|
257
|
+
name: string
|
|
258
|
+
email: string
|
|
259
|
+
}
|
|
260
|
+
const form = useForm<{ rows: Row[] }>({
|
|
261
|
+
initialData: {
|
|
262
|
+
rows: [
|
|
263
|
+
{
|
|
264
|
+
name: 'A',
|
|
265
|
+
email: 'a@a',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const arrayField = form.getField('rows')
|
|
272
|
+
form.getField('rows.0.name')
|
|
273
|
+
form.getField('rows.0.email')
|
|
274
|
+
|
|
275
|
+
// Mimic a "push pristine" flow: push a new item, then anchor it.
|
|
276
|
+
const newItem: Row = {
|
|
277
|
+
name: 'B',
|
|
278
|
+
email: 'b@b',
|
|
279
|
+
}
|
|
280
|
+
form.data.value.rows.push(newItem)
|
|
281
|
+
|
|
282
|
+
// Register the new index's subfields so we can inspect them.
|
|
283
|
+
const newName = form.getField('rows.1.name')
|
|
284
|
+
const newEmail = form.getField('rows.1.email')
|
|
285
|
+
const newRow = form.getField('rows.1')
|
|
286
|
+
|
|
287
|
+
expect(arrayField.dirty.value).toBe(true) // length changed
|
|
288
|
+
|
|
289
|
+
newRow.setInitialData(newItem, { scope: 'subtree' })
|
|
290
|
+
|
|
291
|
+
// The whole new row's subtree reads from the anchor → all clean.
|
|
292
|
+
expect(newName.initialValue.value).toBe('B')
|
|
293
|
+
expect(newEmail.initialValue.value).toBe('b@b')
|
|
294
|
+
expect(newName.dirty.value).toBe(false)
|
|
295
|
+
expect(newEmail.dirty.value).toBe(false)
|
|
296
|
+
expect(newRow.dirty.value).toBe(false)
|
|
297
|
+
|
|
298
|
+
// The array field stays dirty: its baseline still has only the original
|
|
299
|
+
// item (subtree override at rows.1 is invisible to rows).
|
|
300
|
+
expect(arrayField.initialValue.value).toEqual([
|
|
301
|
+
{
|
|
302
|
+
name: 'A',
|
|
303
|
+
email: 'a@a',
|
|
304
|
+
},
|
|
305
|
+
])
|
|
306
|
+
expect(arrayField.dirty.value).toBe(true)
|
|
307
|
+
expect(form.isDirty.value).toBe(true)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('subtree scope does not affect sibling subtrees', () => {
|
|
311
|
+
type Data = {
|
|
312
|
+
a: { v: string }
|
|
313
|
+
b: { v: string }
|
|
314
|
+
}
|
|
315
|
+
const form = useForm<Data>({
|
|
316
|
+
initialData: {
|
|
317
|
+
a: { v: 'a' },
|
|
318
|
+
b: { v: 'b' },
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const aField = form.getField('a')
|
|
323
|
+
const bField = form.getField('b')
|
|
324
|
+
|
|
325
|
+
aField.setInitialData({ v: 'A' }, { scope: 'subtree' })
|
|
326
|
+
|
|
327
|
+
expect(form.getField('a.v').initialValue.value).toBe('A')
|
|
328
|
+
expect(bField.initialValue.value).toEqual({ v: 'b' })
|
|
329
|
+
expect(bField.dirty.value).toBe(false)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('subtree scope: setInitialData on the same field is the natural read path', () => {
|
|
333
|
+
// The field whose path equals the override path *does* read the override
|
|
334
|
+
// (the rule is "skip subtree overrides strictly below the reading path").
|
|
335
|
+
const form = useForm({
|
|
336
|
+
initialData: { user: { name: 'A' } },
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const userField = form.getField('user')
|
|
340
|
+
userField.setInitialData({ name: 'B' }, { scope: 'subtree' })
|
|
341
|
+
|
|
342
|
+
expect(userField.initialValue.value).toEqual({ name: 'B' })
|
|
343
|
+
expect(userField.dirty.value).toBe(false)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('mixed scopes: tree override at parent + subtree override at child', () => {
|
|
347
|
+
const form = useForm({
|
|
348
|
+
initialData: {
|
|
349
|
+
user: {
|
|
350
|
+
profile: {
|
|
351
|
+
name: 'A',
|
|
352
|
+
age: 1,
|
|
353
|
+
},
|
|
354
|
+
settings: { theme: 'light' },
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const userField = form.getField('user')
|
|
360
|
+
const profileField = form.getField('user.profile')
|
|
361
|
+
const nameField = form.getField('user.profile.name')
|
|
362
|
+
|
|
363
|
+
// Tree override at user: globally repoints baseline.
|
|
364
|
+
userField.setInitialData(
|
|
365
|
+
{
|
|
366
|
+
profile: {
|
|
367
|
+
name: 'B',
|
|
368
|
+
age: 2,
|
|
369
|
+
},
|
|
370
|
+
settings: { theme: 'dark' },
|
|
371
|
+
},
|
|
372
|
+
{ scope: 'tree' },
|
|
373
|
+
)
|
|
374
|
+
expect(userField.dirty.value).toBe(false)
|
|
375
|
+
expect(profileField.initialValue.value).toEqual({
|
|
376
|
+
name: 'B',
|
|
377
|
+
age: 2,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Subtree override deeper: should pin only its subtree, but since it lives
|
|
381
|
+
// strictly below the user-tree override, user's view is unaffected.
|
|
382
|
+
nameField.setInitialData('C', { scope: 'subtree' })
|
|
383
|
+
|
|
384
|
+
// The leaf reads its own anchor.
|
|
385
|
+
expect(nameField.initialValue.value).toBe('C')
|
|
386
|
+
expect(nameField.dirty.value).toBe(false)
|
|
387
|
+
|
|
388
|
+
// The profile field (ancestor of the subtree anchor, descendant of the
|
|
389
|
+
// tree override) keeps its tree-override view: name still 'B'.
|
|
390
|
+
expect(profileField.initialValue.value).toEqual({
|
|
391
|
+
name: 'B',
|
|
392
|
+
age: 2,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// The user field (further ancestor) still sees its tree override too.
|
|
396
|
+
expect(userField.initialValue.value).toEqual({
|
|
397
|
+
profile: {
|
|
398
|
+
name: 'B',
|
|
399
|
+
age: 2,
|
|
400
|
+
},
|
|
401
|
+
settings: { theme: 'dark' },
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('subtree scope: external initialData reassignment still wipes it', () => {
|
|
406
|
+
const initialData = ref({ user: { name: 'A' } })
|
|
407
|
+
const form = useForm({ initialData })
|
|
408
|
+
|
|
409
|
+
const userField = form.getField('user')
|
|
410
|
+
userField.setInitialData({ name: 'B' }, { scope: 'subtree' })
|
|
411
|
+
expect(userField.initialValue.value).toEqual({ name: 'B' })
|
|
412
|
+
|
|
413
|
+
initialData.value = { user: { name: 'Z' } }
|
|
414
|
+
expect(userField.initialValue.value).toEqual({ name: 'Z' })
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('subtree scope: cascade drops a deeper subtree anchor when ancestor is rewritten', () => {
|
|
418
|
+
const form = useForm({
|
|
419
|
+
initialData: { user: { name: 'A' } },
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const userField = form.getField('user')
|
|
423
|
+
const nameField = form.getField('user.name')
|
|
424
|
+
|
|
425
|
+
nameField.setInitialData('X', { scope: 'subtree' })
|
|
426
|
+
expect(nameField.initialValue.value).toBe('X')
|
|
427
|
+
|
|
428
|
+
userField.setInitialData({ name: 'B' }, { scope: 'subtree' })
|
|
429
|
+
expect(nameField.initialValue.value).toBe('B')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('overriding a Date replaces it wholesale instead of merging to {}', () => {
|
|
433
|
+
const initial = new Date('2020-01-01T00:00:00Z')
|
|
434
|
+
const next = new Date('2026-05-19T00:00:00Z')
|
|
435
|
+
|
|
436
|
+
const form = useForm({
|
|
437
|
+
initialData: { createdAt: initial },
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const field = form.getField('createdAt')
|
|
441
|
+
field.setInitialData(next)
|
|
442
|
+
|
|
443
|
+
// Without the plain-object guard, merge({}, initial, next) would yield {}
|
|
444
|
+
// because Date has no enumerable own properties. With the guard, the
|
|
445
|
+
// override replaces wholesale.
|
|
446
|
+
expect(field.initialValue.value).toBeInstanceOf(Date)
|
|
447
|
+
expect((field.initialValue.value as Date).toISOString()).toBe(
|
|
448
|
+
next.toISOString(),
|
|
449
|
+
)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('form.reset() rebuilds data from the merged tree (overrides survive)', () => {
|
|
453
|
+
const form = useForm({
|
|
454
|
+
initialData: {
|
|
455
|
+
user: {
|
|
456
|
+
name: 'A',
|
|
457
|
+
email: 'x@x',
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const userField = form.getField('user')
|
|
463
|
+
form.getField('user.name')
|
|
464
|
+
form.getField('user.email')
|
|
465
|
+
|
|
466
|
+
userField.setInitialData({ name: 'B' } as {
|
|
467
|
+
name: string
|
|
468
|
+
email: string
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// Mutate the data away from the override, then reset
|
|
472
|
+
form.data.value.user.name = 'changed'
|
|
473
|
+
form.reset()
|
|
474
|
+
|
|
475
|
+
expect(form.data.value.user.name).toBe('B')
|
|
476
|
+
expect(form.data.value.user.email).toBe('x@x')
|
|
477
|
+
expect(form.isDirty.value).toBe(false)
|
|
478
|
+
})
|
|
479
|
+
})
|
|
@@ -350,4 +350,116 @@ describe('useFieldArray', () => {
|
|
|
350
350
|
expect(fieldArray.items.value[2].item.name).toBe('Third')
|
|
351
351
|
})
|
|
352
352
|
})
|
|
353
|
+
|
|
354
|
+
describe('pushPristine', () => {
|
|
355
|
+
it('adds an item, keeps the array dirty, and marks the new item subfields clean', () => {
|
|
356
|
+
type Row = {
|
|
357
|
+
name: string
|
|
358
|
+
email: string
|
|
359
|
+
}
|
|
360
|
+
const form = useForm<{ rows: Row[] }>({
|
|
361
|
+
initialData: {
|
|
362
|
+
rows: [
|
|
363
|
+
{
|
|
364
|
+
name: 'A',
|
|
365
|
+
email: 'a@a',
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
})
|
|
370
|
+
const fieldArray = useFieldArray(form, 'rows')
|
|
371
|
+
|
|
372
|
+
const newItem: Row = {
|
|
373
|
+
name: 'new',
|
|
374
|
+
email: 'new@x',
|
|
375
|
+
}
|
|
376
|
+
const added = fieldArray.pushPristine(newItem)
|
|
377
|
+
|
|
378
|
+
expect(form.data.value.rows).toEqual([
|
|
379
|
+
{
|
|
380
|
+
name: 'A',
|
|
381
|
+
email: 'a@a',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'new',
|
|
385
|
+
email: 'new@x',
|
|
386
|
+
},
|
|
387
|
+
])
|
|
388
|
+
expect(added.path).toBe('rows.1')
|
|
389
|
+
|
|
390
|
+
// Subfields of the new index are clean against the subtree anchor.
|
|
391
|
+
const newName = form.getField('rows.1.name')
|
|
392
|
+
const newEmail = form.getField('rows.1.email')
|
|
393
|
+
expect(newName.initialValue.value).toBe('new')
|
|
394
|
+
expect(newEmail.initialValue.value).toBe('new@x')
|
|
395
|
+
expect(newName.dirty.value).toBe(false)
|
|
396
|
+
expect(newEmail.dirty.value).toBe(false)
|
|
397
|
+
|
|
398
|
+
// The array field itself stays dirty — the array baseline does not see
|
|
399
|
+
// the subtree anchor.
|
|
400
|
+
expect(fieldArray.field.dirty.value).toBe(true)
|
|
401
|
+
expect(form.isDirty.value).toBe(true)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('subfields registered before pushPristine become clean once the anchor is set', () => {
|
|
405
|
+
type Row = { name: string }
|
|
406
|
+
const form = useForm<{ rows: Row[] }>({
|
|
407
|
+
initialData: { rows: [{ name: 'A' }] },
|
|
408
|
+
})
|
|
409
|
+
const fieldArray = useFieldArray(form, 'rows')
|
|
410
|
+
|
|
411
|
+
// Pre-register the would-be path before the item exists.
|
|
412
|
+
const newName = form.getField('rows.1.name')
|
|
413
|
+
|
|
414
|
+
fieldArray.pushPristine({ name: 'new' })
|
|
415
|
+
|
|
416
|
+
expect(newName.data.value).toBe('new')
|
|
417
|
+
expect(newName.initialValue.value).toBe('new')
|
|
418
|
+
expect(newName.dirty.value).toBe(false)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('multiple pushPristine calls anchor each new index independently', () => {
|
|
422
|
+
type Row = { name: string }
|
|
423
|
+
const form = useForm<{ rows: Row[] }>({
|
|
424
|
+
initialData: { rows: [] },
|
|
425
|
+
})
|
|
426
|
+
const fieldArray = useFieldArray(form, 'rows')
|
|
427
|
+
|
|
428
|
+
fieldArray.pushPristine({ name: 'one' })
|
|
429
|
+
fieldArray.pushPristine({ name: 'two' })
|
|
430
|
+
|
|
431
|
+
expect(form.data.value.rows).toEqual([{ name: 'one' }, { name: 'two' }])
|
|
432
|
+
expect(form.getField('rows.0.name').dirty.value).toBe(false)
|
|
433
|
+
expect(form.getField('rows.1.name').dirty.value).toBe(false)
|
|
434
|
+
expect(fieldArray.field.dirty.value).toBe(true)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('editing a pushPristine item after the fact marks that subfield dirty', () => {
|
|
438
|
+
type Row = { name: string }
|
|
439
|
+
const form = useForm<{ rows: Row[] }>({
|
|
440
|
+
initialData: { rows: [] },
|
|
441
|
+
})
|
|
442
|
+
const fieldArray = useFieldArray(form, 'rows')
|
|
443
|
+
|
|
444
|
+
fieldArray.pushPristine({ name: 'initial' })
|
|
445
|
+
const nameField = form.getField('rows.0.name')
|
|
446
|
+
expect(nameField.dirty.value).toBe(false)
|
|
447
|
+
|
|
448
|
+
nameField.setData('edited')
|
|
449
|
+
expect(nameField.dirty.value).toBe(true)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('regular push leaves the item subfields dirty (contrast)', () => {
|
|
453
|
+
type Row = { name: string }
|
|
454
|
+
const form = useForm<{ rows: Row[] }>({
|
|
455
|
+
initialData: { rows: [] },
|
|
456
|
+
})
|
|
457
|
+
const fieldArray = useFieldArray(form, 'rows')
|
|
458
|
+
|
|
459
|
+
fieldArray.push({ name: 'plain' })
|
|
460
|
+
// Without the subtree anchor, the new index's baseline is external
|
|
461
|
+
// (undefined at rows.0.name) → field is dirty.
|
|
462
|
+
expect(form.getField('rows.0.name').dirty.value).toBe(true)
|
|
463
|
+
})
|
|
464
|
+
})
|
|
353
465
|
})
|