Skip to content

Commit 8369371

Browse files
Fixes a bug that caused dom nodes in nested grouped fields to be improperly re-used when removing a lower index item
1 parent 1ed0542 commit 8369371

File tree

8 files changed

+216
-14
lines changed

8 files changed

+216
-14
lines changed

dist/formulate.esm.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/formulate.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/formulate.umd.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/FormulateSpecimens.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
id="app"
3030
class="specimen-list"
3131
>
32+
<Test />
3233
<SpecimenButton />
3334
<SpecimenBox />
3435
<SpecimenFile />
@@ -51,10 +52,12 @@ import SpecimenButton from './specimens/SpecimenButton'
5152
import SpecimenBox from './specimens/SpecimenBox'
5253
import SpecimenSlider from './specimens/SpecimenSlider'
5354
import SpecimenSelect from './specimens/SpecimenSelect'
55+
import Test from './specimens/Test'
5456
5557
export default {
5658
name: 'App',
5759
components: {
60+
Test,
5861
SpecimenButton,
5962
SpecimenBox,
6063
SpecimenText,

examples/specimens/Test.vue

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<template>
2+
<div>
3+
<div class="demo">
4+
<!-- debug the model variable here for reference -->
5+
<div class="red">
6+
<div>DEBUG: groupModel is currently:</div>
7+
<pre
8+
v-text="groupModel"
9+
/>
10+
</div>
11+
<!-- main form container -->
12+
<FormulateForm>
13+
<!-- the grouping input, with a local v-model specific to this group -->
14+
<FormulateInput
15+
v-model="groupModel"
16+
type="group"
17+
name="question"
18+
:repeatable="false"
19+
>
20+
<!-- for loop to output each checkbox of the "question" mini-schema -->
21+
<FormulateInput
22+
v-for="child in question.children"
23+
:key="child.name"
24+
v-bind="child"
25+
/>
26+
</FormulateInput>
27+
</FormulateForm>
28+
<!-- button to try to change the values of the model -->
29+
<button @click="setNewValues">
30+
Set New Values
31+
</button>
32+
</div>
33+
</div>
34+
</template>
35+
36+
<script>
37+
export default {
38+
data () {
39+
return {
40+
groupModel: undefined, // initialise the model to nothing
41+
question: {
42+
// the children items for this question, which is a list of checkboxes
43+
children: [
44+
{
45+
name: 'course_A',
46+
type: 'checkbox',
47+
label: 'Label for Course A',
48+
options: [
49+
{
50+
value: 'ABC',
51+
label: 'This is the ABC option'
52+
},
53+
{
54+
value: 'DEF',
55+
label: 'This is the DEF option'
56+
}
57+
]
58+
},
59+
{
60+
name: 'course_B',
61+
type: 'checkbox',
62+
label: 'Label for Course B',
63+
options: [
64+
{
65+
value: 'XYZ',
66+
label: 'This is the XYZ option'
67+
}
68+
]
69+
}
70+
]
71+
}
72+
73+
}
74+
},
75+
methods: {
76+
setNewValues () {
77+
// PROBLEM: attempting to set the groupModel variable to a new value to update the group does not actually update the group, and gets overwritten to empty strings again:
78+
console.log('doing setting new values')
79+
this.$set(this, 'groupModel', [
80+
{
81+
course_A: ['ABC', 'DEF'],
82+
course_B: ['XYZ']
83+
}
84+
])
85+
// can also try directly:
86+
this.groupModel = [
87+
{
88+
course_A: ['ABC', 'DEF'],
89+
course_B: ['XYZ']
90+
}
91+
]
92+
}
93+
}
94+
}
95+
</script>

src/FormulateGrouping.vue

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
>
88
<FormulateRepeatableProvider
99
v-for="(item, index) in items"
10-
:key="index"
10+
:key="item.__id"
1111
:index="index"
1212
:context="context"
1313
:uuid="item.__id"
@@ -21,7 +21,7 @@
2121
</template>
2222

2323
<script>
24-
import { setId, has } from './libs/utils'
24+
import { setId, has, equals } from './libs/utils'
2525
2626
export default {
2727
name: 'FormulateGrouping',
@@ -40,7 +40,8 @@ export default {
4040
},
4141
data () {
4242
return {
43-
providers: []
43+
providers: [],
44+
keys: []
4445
}
4546
},
4647
inject: ['formulateRegisterRule', 'formulateRemoveRule'],
@@ -49,16 +50,16 @@ export default {
4950
if (Array.isArray(this.context.model)) {
5051
if (!this.context.repeatable && this.context.model.length === 0) {
5152
// This is the default input.
52-
return [setId({})]
53+
return [this.setId({}, 0)]
5354
}
5455
if (this.context.model.length < this.context.minimum) {
5556
return (new Array(this.context.minimum || 1)).fill('')
56-
.map((t, index) => setId(this.context.model[index] || {}))
57+
.map((t, index) => this.setId(this.context.model[index] || {}, index))
5758
}
58-
return this.context.model.map(item => setId(item))
59+
return this.context.model.map((item, index) => this.setId(item, index))
5960
}
6061
// This is an unset group
61-
return (new Array(this.context.minimum || 1)).fill('').map(() => setId({}))
62+
return (new Array(this.context.minimum || 1)).fill('').map((_i, index) => this.setId({}, index))
6263
},
6364
formShouldShowErrors () {
6465
return this.context.formShouldShowErrors
@@ -78,6 +79,14 @@ export default {
7879
if (val) {
7980
this.showErrors()
8081
}
82+
},
83+
items: {
84+
handler (items, oldItems) {
85+
if (!equals(items, oldItems, true)) {
86+
this.keys = items.map(item => item.__id)
87+
}
88+
},
89+
immediate: true
8190
}
8291
},
8392
created () {
@@ -100,12 +109,11 @@ export default {
100109
this.providers.forEach(p => p && typeof p.showErrors === 'function' && p.showErrors())
101110
},
102111
setItem (index, groupProxy) {
103-
const id = this.items[index].__id || false
104112
// Note: value must have an __id to use this function
105113
if (Array.isArray(this.context.model) && this.context.model.length >= this.context.minimum) {
106-
this.context.model.splice(index, 1, setId(groupProxy, id))
114+
this.context.model.splice(index, 1, this.setId(groupProxy, index))
107115
} else {
108-
this.context.model = this.items.map((item, i) => i === index ? setId(groupProxy, id) : item)
116+
this.context.model = this.items.map((item, i) => i === index ? this.setId(groupProxy, index) : item)
109117
}
110118
},
111119
removeItem (index) {
@@ -115,7 +123,7 @@ export default {
115123
this.context.rootEmit('repeatableRemoved', this.context.model)
116124
} else if (!Array.isArray(this.context.model) && this.items.length > this.context.minimum) {
117125
// In this context the fields have never been touched (not "dirty")
118-
this.context.model = (new Array(this.items.length - 1)).fill('').map(() => setId({}))
126+
this.context.model = (new Array(this.items.length - 1)).fill('').map((_i, idx) => this.setId({}, idx))
119127
this.context.rootEmit('repeatableRemoved', this.context.model)
120128
}
121129
// Otherwise, do nothing, we're at our minimum
@@ -127,6 +135,9 @@ export default {
127135
},
128136
deregisterProvider (provider) {
129137
this.providers = this.providers.filter(p => p !== provider)
138+
},
139+
setId (item, index) {
140+
return item.__id ? item : setId(item, this.keys[index])
130141
}
131142
}
132143
}

src/slots/FormulateRepeatable.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<FormulateSlot
77
name="remove"
88
:context="context"
9+
:index="index"
910
:remove-item="removeItem"
1011
>
1112
<component

test/unit/FormulateInputGroup.test.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,4 +967,96 @@ describe('FormulateInputGroup', () => {
967967
expect(wrapper.find('.formulate-input-element > span').text()).toBe('$money')
968968
expect(wrapper.find('.formulate-input-element > *:last-child').text()).toBe('after money')
969969
})
970+
971+
it('is able to set/remove the proper values when using nested repeatable groups', async () => {
972+
const wrapper = mount({
973+
template: `
974+
<FormulateInput
975+
v-model="users"
976+
name="users"
977+
type="group"
978+
:repeatable="true"
979+
>
980+
<template #remove="{ removeItem }">
981+
<a href="" class="remove-user" @click.prevent="removeItem">Remove</a>
982+
</template>
983+
<FormulateInput
984+
type="text"
985+
name="username"
986+
/>
987+
<FormulateInput
988+
type="group"
989+
name="social"
990+
:repeatable="true"
991+
>
992+
<FormulateInput
993+
name="platform"
994+
type="select"
995+
:options="['Twitter', 'Facebook', 'Instagram']"
996+
/>
997+
<FormulateInput
998+
name="handles"
999+
type="group"
1000+
:options="['Twitter', 'Facebook', 'Instagram']"
1001+
>
1002+
<FormulateInput
1003+
type="text"
1004+
name="handle"
1005+
/>
1006+
</FormulateInput>
1007+
</FormulateInput>
1008+
</FormulateInput>
1009+
`,
1010+
data () {
1011+
return {
1012+
users: [
1013+
{
1014+
username: 'Jon',
1015+
social: [{ platform: 'Twitter', handles: [{ handle: '@jon' }] }, { platform: 'Facebook', handles: [{ handle: '@fb-jon' }] }]
1016+
},
1017+
{
1018+
username: 'Jane',
1019+
social: [{ platform: 'Instagram', handles: [{ handle: '@jane' }] }, { platform: 'Facebook', handles: [{ handle: '@fb-jane' }] }]
1020+
}
1021+
]
1022+
}
1023+
}
1024+
})
1025+
await flushPromises()
1026+
wrapper.findAll('[name="username"]').wrappers.map(wrapper => wrapper.element.value)
1027+
// Make sure the top level fields have the right values
1028+
expect(
1029+
wrapper.findAll('[name="username"]').wrappers.map(wrapper => wrapper.element.value)
1030+
).toEqual(['Jon', 'Jane'])
1031+
1032+
// Make sure the secondary depth fields have the right values
1033+
expect(
1034+
wrapper.findAll('select').wrappers.map(wrapper => wrapper.element.value)
1035+
).toEqual(['Twitter', 'Facebook', 'Instagram', 'Facebook'])
1036+
1037+
// Remove the first user
1038+
wrapper.find('.remove-user').trigger('click')
1039+
await flushPromises()
1040+
1041+
// Expect the first username to now be the second user
1042+
expect(
1043+
wrapper.findAll('[name="username"]').wrappers.map(wrapper => wrapper.element.value)
1044+
).toEqual(['Jane'])
1045+
1046+
expect(
1047+
wrapper.findAll('select').wrappers.map(wrapper => wrapper.element.value)
1048+
).toEqual(['Instagram', 'Facebook'])
1049+
1050+
wrapper.find('.formulate-input-group-repeatable-remove').trigger('click')
1051+
await flushPromises()
1052+
1053+
expect(
1054+
wrapper.findAll('select').wrappers.map(wrapper => wrapper.element.value)
1055+
).toEqual(['Facebook'])
1056+
1057+
expect(
1058+
wrapper.findAll('[name="handle"]').wrappers.map(wrapper => wrapper.element.value)
1059+
).toEqual(['@fb-jane'])
1060+
})
1061+
9701062
})

0 commit comments

Comments
 (0)