How to make Vue-Draggable work with different structure of elements/components
Tags: sortablejs, vuedraggable, vue, components
Published: 2020-06-13 11:57:14

Recently I have a task where I need to create a list of draggable items, so instead of creating my own draggable component I search for an already existing one, and in the end I choose vue-draggable. It's based on Sortable.js, and Sortable.js has been used by so many people, and could be said more mature than other library.

The problem I get when doing my task was, when creating a draggable list using vue-draggable you need to pass in a `list` or `value` property so it can be matching with the element list. This is the example,
```vue
<draggable tag="ul" :list="somelist">
    <li v-for="item in somelist" :key="item.id">
        {{ item.data }}
    </li>
</draggable>
```
This is good if you have the same tipe of elements to loop trough, but what if you have a different type of element, and the structure are also differents? Like for example,
```vue
<draggable tag="div" class="container">
    <div class="firstChild">Test</div>
    <div class="secondChild">
        <span>Hello</span>
    </div>
</draggable>
```
This is still work like normal, you can drag & drop them, but the problem are, what if you want to use `move` event props? You can't. I already tried them, I don't know if there's something wrong with my implementation, but `move` event wouldn't be called if the `list` or `value` are not get passed in. And, even if you try to pass `list` or `value` props, like this,
```vue
<draggable tag="div" :list="somelist" class="container">
    <div class="firstChild">Test</div>
    <div class="secondChild">
        <span>Hello</span>
    </div>
</draggable>
```
`somelist` data looks like this,
```js
data: () => ({
    somelist: ['first', 'second']
})
```
It wouldn't work. If you try to drag & drop them it would just go back to it's previous place.

But, after many hours of research and trying, I finally found a workaround to solve this, I don't know if this is efficient or a good way of solving this, but it works like what I expected.

So what I did was actually pretty simple, we create a wrapper component for draggable, and using functional rendering to manually render the children elements, and lastly creating `v-model` like functionality in `render` method. Okay, let me just show you how I do it step by step.

#### # Create a wrapper component
Instead of using a `draggable` component, I create a small component and use the `draggable` component there instead. Also instead of using template render, I'm using a `render` method to render the element manually. Like this,
```vue
<script>
export default {
    render (h) {
        return h('draggable', { 
            props: { ...this.$attrs }
        }, this.$slots.default)
    }
}
</script>
```

#### # Use `mounted` method to save the list of elements
After that we use `mounted` method to save the children elements in a `list` property where we can use this later in the `render` method. Like this,
```js
data: () => ({
    list: [] // You can name this whatever you want
}),
mounted () {
    // You can change this `key` variable to whatever you want, 
    // but it must be unique.
    let key = 0 
    const filtered = this.$slots.default.filter(
      vnode => vnode.tag !== undefined
    )
    
    this.list = filtered.map(vnode => ({ id: key++, vnode }))
}
```
As you can see we first filter the children, so we actually not includes the TextNode element. And after that we just mapping the array to some object that would be used later.

#### # Add `input` event to render method
Like what I said before we want to create `v-model` like logic/functionality to make sure the list data are get binded with the elements. It's actually not that hard, I just followed like in [the docs](https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components). Like this,
```js
render (h) {
    return h('draggable', { 
        props: { ...this.$attrs, value: this.list },
        on: { input: ($event) => { this.list = $event } }
    }, this.list.map(el => {
       el.vnode.key = el.id
       return el.vnode
    }))
}
```
Now if you try run this,
```vue
<wrapper-draggable tag="div" class="container">
    <div class="firstChild">Test</div>
    <div class="secondChild">
        <span>Hello</span>
    </div>
</wrapper-draggable>
```
Now, it would be possible to drag & drop the elements and at the same time also have `move` event to be called.

#### # Full Code
```vue
<script>
import draggable from 'vuedraggable'

export default {
    components: { draggable }
    data: () => ({
        list: [] // You can name this whatever you want
    }),
    mounted () {
        // You can change this `key` variable to whatever you want, 
        // but it must be unique.
        let key = 0 
        const filtered = this.$slots.default.filter(
          vnode => vnode.tag !== undefined
        )
        this.list = filtered.map(vnode => ({ id: key++, vnode }))
    },
    render (h) {
        return h('draggable', { 
            props: { ...this.$attrs, value: this.list },
            on: { input: ($event) => { this.list = $event } }
        }, this.list.map(el => {
           el.vnode.key = el.id
           return el.vnode
        }))
    }
}
</script>
```

#### # End Note
You may realize that with this workaround we cannot pass `list` or `value` props, but I think with some changes in the code we could make it possible to do that. Like you can just change the `list` data property to something else, like `listVal` or something, and add `list` property to `props` and then assigned the `list` props to that `listVal` data property.

I found out that you actually can pass Sortable's option as vue props (like what vue-draggable recommended). But we need to change the codes a little.

First, you need to move out all the vue-draggable props (like `tag`, `options`, `value`, and etc. You can see the full [list here](https://github.com/SortableJS/Vue.Draggable/blob/2df59aecda6901d56a83bfd299e67973482271c5/src/vuedraggable.js#L108).) into the component `props` property (See above for the types). [You can see here for the full code](https://gist.github.com/athif23/1b856259df4219374e5ac0df4ece4f0f).

With this, now you can pass Sortable's options as a vue props/attrs, like this, `<wrapper-draggable filter=".some-item">`.

And that's the end of this article, I hope you found this useful, thanks for reading :)