Ad

How To Keep Sortable Arrays And Components In Order In Vue.js

- 1 answer

I'm using Vue.js and Dragula to make a drag and drop tile page. Each tile contains its own set of data, so each tile is a Vue component.

The problem is that once I drag and drop one of the tiles, the DOM elements and the data array in the Vue instance fall out of sync and start to cause problems. Just dragging and dropping doesn't create a problem, but once I drag and drop something and then try to delete it, everything goes wrong.

Here's a fiddle: https://jsfiddle.net/wfjawvnL/13/

Here's my HTML, with the component template:

<body>
  <div id="app">
    <div id="draggable" class="wrapper">
      <template v-for="(index, item) in list">
        <block :id="index" :index="index" :item="item" :name="item.name">
        </block>
      </template>
    </div>
    <div class="footer">
      <pre>{{ $data | json }}</pre>
    </div>
  </div>
</body>

<template id="block-template">
  <div :id="index" :class="[name, 'block']">
    <div class="text name">{{ name }}</div>
    <div>Index: {{ index }}</div>
    <span class="delete" v-on:click="removeItem(item)">&#x2717;</span>
  </div>
</template> 

Here's my Vue instance:

var vm = new Vue({
    el: '#app',

        data: {
        list: [
        {name: 'item1'},
        {name: 'item2'},
        {name: 'item3'},
        {name: 'item4'},
        {name: 'item5'}
      ]
    },

    methods: {
        reorder: function (element, sibling) {
            var children = element.parentNode.querySelectorAll(".block");

            var length = this.list.length;

            var ids = [];

            for (var i = 0; i < length; i++) {
                if (children[i].id) {
                    ids.push(children[i].id);
                }

                children[i].id = i;
            }

            var vm = this;

            var newArr = ids.map(function (id) {
                return vm.list[id];
            });

            this.list = newArr;
        }
    }
}); 

Here's the component:

Vue.component('block', {
    template: '#block-template',  

    props: ['index', 'name', 'item'],

    methods: {
        removeItem: function (item) {
           vm.list.$remove(item);
        }
    }
});

And I'm calling Dragula like this:

dragula([document.getElementById('draggable')]).on('drop', function (el, target, source, sibling) {
    vm.reorder(el, sibling);
});

This fiddle works fine but doesn't use components: https://jsfiddle.net/z2s83yfL/1/

Ad

Answer

I had two problems. First, I was binding the array index and using it as an id. The index was being changed, which was also causing the id to change. Second, using v-for on a template tag was causing some issues. I changed the tags to divs and it now works.

Working fiddle: https://jsfiddle.net/wfjawvnL/18/

Vue stuff:

Vue.component('block', {
    template: '#block-template',  

    props: ['name', 'item'],

    methods: {
        removeItem: function (item) {
           vm.list.$remove(item);
        }
    }
});

var vm = new Vue({
    el: '#app',

        data: {
        list: [
        {name: 'item1'},
        {name: 'item2'},
        {name: 'item3'},
        {name: 'item4'},
        {name: 'item5'}
      ]
    },

    ready: function() {
        var self = this;
        var from = null;
        var drake = dragula([document.querySelector('#draggable')]);

        drake.on('drag', function(element, source) {
        var index = [].indexOf.call(element.parentNode.children, element);

        from = index;
        });

        drake.on('drop', function(element, target, source, sibling) {
        var index = [].indexOf.call(element.parentNode.children, element);

        self.list.splice(index, 0, self.list.splice(from, 1)[0]);
        });
    }
});

HTML stuff:

<body>
  <div id="app">
    <div id="draggable" class="wrapper">
      <div class="wrapper" v-for="item in list">
        <block :item="item" :name="item.name">
        </block>
      </div>
    </div>
    <pre>{{ $data | json }}</pre>
  </div>
</body>

<template id="block-template">
  <div :class="[name, 'block']">
    <div class="text name" v-on:click="removeItem(item)">{{ name }}</div>
    <span class="delete" v-on:click="removeItem(item)">&#x2717;</span>
  </div>
</template>
Ad
source: stackoverflow.com
Ad