I'm introducing myself to Vue.js (version 2.5.16) but am running into a problem understanding what looks like a problem with Reactivity:
I have two lists of components with v-for loops that render correctly most of the time - I can interact with the components and add new components. The components interact with a REST API representing 'zones', and whose function I'm hoping is rather obvious - It mostly returns JSON representations of either a single or all 'zones' for example:
{
"state": "off",
"pin": 24,
"uri": "/zones/6",
"name": "extra"
}
Whenever a zone is added or removed, the app reloads the entire list of zones. However, for some reason, when I delete a zone and trigger a reload of the zones, the list gets rendered with the wrong data! No matter which zone I delete, the last item of the list appears to be 'popped', rather than the expected zone not showing up on the new list! When I inspect the app.zones data, everything appears correct in the Javascript data but it just doesn't show up right in the browser.
Here's the relevant code:
...
<div class="tab-content">
<div id="control" class="tab-pane fade in active">
<ul>
<zone-control
v-for="zone in zones"
v-bind:init-zone="zone"
v-bind:key="zone.id">
</zone-control>
</ul>
</div>
<div id="setup" class="tab-pane fade">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Pin</th>
<th></th>
</tr>
</thead>
<tbody>
<tr is="zone-setup"
v-for="zone in zones"
v-bind:init-zone="zone"
v-bind:key="zone.id">
</tr>
<tr is="zone-add"></tr>
</tbody>
</table>
</div>
</div>
...
<script>
Vue.component('zone-control', {
props: ['initZone'],
template:
`<li>
<div class="btn btn-default" v-on:click="toggleState">
<span v-if="zone.state == 'on'" class="glyphicon glyphicon-ok-circle text-success"></span>
<span v-else class="glyphicon glyphicon-remove-sign text-danger"></span>
Zone: {{ zone.name }}
</div>
</li>`,
data: function() {
return {
zone: this.initZone
};
},
methods: {
toggleState: function() {
var state = (this.zone.state == 'on' ? 'off' : 'on');
console.log('Turning zone ' + this.zone.name + ' ' + state);
var comp = this
fetch(
this.zone.uri,
{method: 'PUT',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: JSON.stringify({state: state})
}).then( function(result) {
return result.json()
}).then( function(data) {
comp.zone = data;
});
}
}
})
Vue.component('zone-setup', {
props: ['initZone'],
template:
`<tr>
<td>{{ zone.name }}</td>
<td>{{ zone.pin }}</td>
<td><div v-on:click="deleteZone" class="btn btn-danger btn-small"></div></td>
</tr>`,
data: function() {
return {
zone: this.initZone
};
},
methods: {
deleteZone: function() {
fetch(
this.zone.uri,
{method: 'DELETE'}
).then( function(result) {
app.load_zones();
});
}
}
})
Vue.component('zone-add', {
template:
`<tr>
<td><input v-model="zone.name" type="text" class="form-control"></input></td>
<td><input v-model="zone.pin" type="text" class="form-control"></input></td>
<td><div v-on:click="addZone" class="btn btn-success btn-small"></div></td>
</tr>`,
data: function() {
return {
zone: {
name: '',
pin: ''
}
};
},
methods: {
addZone: function() {
console.log('Adding zone ' + this.zone.name);
var comp = this
fetch(
"/zones",
{
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: JSON.stringify({
name: comp.zone.name,
pin: comp.zone.pin})
}
).then( function(result) {
app.load_zones()
comp.zone = {}
});
}
}
})
var app = new Vue({
el: '#app',
data: {
zones: []
},
methods: {
load_zones: function() {
fetch("zones")
.then(function(result){
return result.json()
}).then(function(data){
app.zones = data;
});
}
},
created: function() {
this.load_zones();
}
})
</script>
I've read through several references and there seem to be some edge cases or other 'gatchas' but this doesn't seem to fall into any of those. For example, this other question seems to agree that replacing the whole array is a pretty fool-proof way of altering arrays within the constraints of the Vue.js reactivity system.
Some other links to Vue.js documentation that I've checked and that are relevant but don't seem to point to my problem:
For the full code in context, see the github repo here.
UPDATE
Per the comments, I've reproduced the symptoms in a simplified JSBin. Click on any the button to see that, when the second element should be removed, the last one gets removed instead!
Vue.set(app, zones, data);rather thanapp.zones = data?Vue.set(app, 'zones', data);. No change in behavior.