How to make Vue-Draggable work with different structure of elements/components
At Indo
Posted on June 13, 2020
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 :)
Posted on June 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
June 13, 2020