DEV Community

At Indo
At Indo

Posted on

How to make Vue-Draggable work with different structure of elements/components

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,

<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,

<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,

<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,

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,

<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,

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. Like this,

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,

<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

<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.) into the component props property (See above for the types). You can see here for the full code.

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 :)

Top comments (0)