Richard Vanbergen
Richard Vanbergen

Reputation: 1974

Vue removing wrong HTML node in dynamic list of components

I'm experimenting with Vue.JS and composing components together with dynamically.

There's a strange issue where although it seems to be updating the data correctly, if I remove one of the boxes with the call to splice() it always removes the last item in the rendered HTML.

Here's an example fiddle. I'm testing in Chrome.

https://jsfiddle.net/afz6jjn0/

Just for posterity, here's the Vue component code:

Vue.component('content-longtext', {
  template: '#content-longtext',
  props: {
    model: { type: String, required: true },
    update: { type: Function, required: true }
  },
  data() {
    return {
      inputData: this.model
    }
  },
  methods: {
    updateContent(event) {
      this.update(event.target.value)
    }
  },
})

Vue.component('content-image', {
  template: '#content-image',
})

Vue.component('content-list', {
  template: '#content-list-template',
  props: {
    remove: { type: Function, required: true },
    update: { type: Function, required: true },
    views: { type: Array, required: true }
  },
  methods: {
    removeContent(index) {
      this.remove(index)
    },
    updateContent(index) {
      return (content) => this.update(index, content)
    },
  },
})

Vue.component('content-editor', {
  template: '#content-editor',
  data() {
    return {
      views: [
        {type: 'content-longtext', model: 'test1'},
        {type: 'content-longtext', model: 'test2'},
        {type: 'content-longtext', model: 'test3'},
        {type: 'content-longtext', model: 'test4'},
        {type: 'content-longtext', model: 'test5'},
      ],
    }
  },
  methods: {
    newContentBlock(type) {
      this.views.push({type: 'content-longtext', model: ''})
    },
    updateContentBlock(index, model) {
      this.views[index].model = model
    },
    removeContentBlock(index) {
      this.views.splice(index, 1)
    },
  },
})

let app = new Vue({
  el: '#app'
})

Upvotes: 0

Views: 723

Answers (1)

Richard Vanbergen
Richard Vanbergen

Reputation: 1974

I've managed to fix the issue thanks to this documentation.

The crux of it is if you don't have a unique key already, you need to store the array index of the object in the object itself, this is because as you mutate the source array you are also mutating it's keys and as far a Vue is concerned when it renders, the last item is missing, not the removed item.

views: [
  {index: 0, type: 'content-longtext', model: 'test1'},
  {index: 1, type: 'content-longtext', model: 'test2'},
  {index: 2, type: 'content-longtext', model: 'test3'},
  {index: 3, type: 'content-longtext', model: 'test4'},
  {index: 4, type: 'content-longtext', model: 'test5'},
],

...

newContentBlock(type) {
  this.views.push({index: this.views.length, type: 'content-longtext', model: ''})
},

Once you have stored the array index you need to add the :key binding to the iterator in the template, and bind that stored value.

<div v-for="(currentView, index) in views" :key="currentView.index">
  <component :is="currentView.type" :model="currentView.model" :update="updateContent(index)"></component>
  <a v-on:click="removeContent(index)">Remove</a>
</div>

Finally you must make sure you preserve the integrity of your indexes when you mutate the array.

removeContentBlock(index) {
  this.views
    .splice(index, 1)
    .map((view, index) => view.index = index)
},

https://jsfiddle.net/afz6jjn0/5/

Upvotes: 4

Related Questions