👌 Drag and drop so simple it hurts
Vue wrapper for dragula drag'n drop library, based on vue-dragula by @Astray-git.
This library has been refactored, upgraded and extended with powerful new features for use with Vue 2.
This library has a long standing issue with the copy
operation, resulting in items being inserted twice in the target container.
I have tried to debug it thorougly but with no success so far. I suspect it has to do with the Vue2 override of Array splice
method, used in the ModelManager
and the correct use of the Vue update queue.
You can try the ImmutableModelManager
and see if that works better as it will return a new immutable array with a new pointer on each change.
$set for modifying objects in array for reactivity
Vue.set(this.model, changedModel)
Here a JSFiddle to play with
Please help fix this bug!
You can also try a simple array example in a Vue component, with buttons and handlers to simulate each of the effects (on underlying array model) for the drag effects:
copy
move
To better help track and fix the problem with the way Vue updates arrays.
insertAt(index, insertModel) {
const splicedModel = this.model.splice(index, 0, insertModel)
const modelAfterInsert = this.model
return splicedModel
}
Currently the splicedModel
returns an empty array []
and modelAfterInsert
the same (unmodified) array as before the splice
. Yet, copy
(or Vue?) still ends up inserting the item twice in the UI
When making a copy error we need to ensure we are not reusing the same reference in the two container models. We need to clone the value first.
Otherwise, if "copy" is deleted or modified in one container, it will be deleted/modifed in both due to shared reference.
dropModelTarget(dropElm, target, source) {
let notCopy = this.dragElm === dropElm
let dropElmModel = notCopy ? this.dropElmModel() : this.jsonDropElmModel()
if (notCopy) {
this.notCopy()
}
// ...
}
jsonDropElmModel() {
// ...
let jsonStr = JSON.stringify(stringable || model)
return JSON.parse(jsonStr)
}
So we should be handling this correctly!?
I haven't been using Vue2 much for the past year, so could use one or more contributors to be take care of this project and keep it up to date. Thanks!
- Works with Vue 2
- More flexible and powerful than original (Vue 1) plugin
- Removed concept of bags. Reference named drakes directly
- Vue2 demo app
See Changelog for details.
Beware of Vue 2 reactivity issues when working with Arrays.
See post: reactivity in Vue2 vs Vue3
Use Vue.set
or vm.$set
to explicitly set/initialize an Array on a component and notify Vue about it.
Beware of pointers and instances which can lead to bugs where a service is triggered multiple times, leading to duplication of events.
npm install vue2-dragula --save
yarn add vue2-dragula
import Vue from 'vue'
import { Vue2Dragula } from 'vue2-dragula'
Vue.use(Vue2Dragula, {
logging: {
service: true // to only log methods in service (DragulaService)
}
});
Dragula's CSS, which provides visual feedback for drag effects, is not included in this package and must be imported or provided in your app.
import 'dragula/dist/dragula.css'
For additional documentation, see the docs folder
See Dragula events and drag effects
<div class="wrapper">
<div class="container" v-dragula="colOne" drake="first">
<!-- with click -->
<div v-for="text in colOne" :key="text" @click="onClick">{{text}} [click me]</div>
</div>
<div class="container" v-dragula="colTwo" drake="first">
<div v-for="text in colTwo" :key="text">{{text}}</div>
</div>
</div>
NOTE: Since Vue 2.x, having the :key
attribute when using v-for
is reqired.
You can access the global app service via Vue.$dragula.$service
or from within a component via this.$dragula.$service
(recommended for most scenarios).
You can also create named services for more fine grained control.
Set dragula options
Use service.options(name, options)
to configure service options
// ...
new Vue({
// ...
created () {
const service = Vue.$dragula.$service
service.options('my-drake', {
direction: 'vertical'
})
}
})
Use service.find(name)
to return a named drake
instance registered with the service.
See drake events
Use service.eventBus.$on
to define drake event handlers
service.eventBus.$on('drop', (args) => {
console.log('drop: ' + args[0])
})
npm
scripts included:
npm run build
to build new distribution in/dist
npm run dev
run example in dev modenpm run lint
lint code using ESlint
Access this.$dragula
in your created () { ... }
life cycle hook of any component which uses the v-dragula
directive.
Add a named service via this.$dragula.createService({name, eventBus, drakes})
factory method. Initialise each service with the drakes you want to use.
$dragula
API:
createService({name, eventBus, drakes})
: to create a named servicecreateServices({names, ...})
: to create multiple services (names
list)on(handlerConfig = {})
: add event handlers to all serviceson(name, handlerConfig = {})
: add event handlers to specific servicedrakesFor(name, drakes = {})
: configure a service with drakesservice(name)
: get named service.services
: get list of all registered services.serviceNames
: get list of names for all registered services
The DragulaService
constructor takes the following deconstructed arguments.
Only name
and eventBus
are required.
Note: You don't normally need to create the DragulaService
yourself. Use the API to handle this for you.
class DragulaService {
constructor ({name, eventBus, drakes, options}) {
...
}
// ...
}
Drakes are indexed by name in the drakes
Object of the service. Each key is the name of a drake which points to a drake
instance. The drake
can have event handlers, models, containers etc. See dragula options
Please note that vue-dragula
expects the v-dragula
binding expression to point to a model in the VM of the component, ie. v-dragula="items"
When you move the elements in the UI you also (by default) rearrange the underlying model list items (using findModelForContainer
in the service). This is VERY powerful!
Note that special Vue events removeModel
, dropModel
and insertAt
are emitted as model items are moved around.
this.name, el, source, this.dragIndex
'my-first:removeModel': ({name, el, source, dragIndex, sourceModel}) => {
// ...
},
'my-first:dropModel': ({name, el, source, target, dropIndex, sourceModel}) => {
// ...
},
'my-first:insertAt': ({indexes, models, elements}) => {
// ...
},
el
main DOM element of element (f.ex element being dropped on)source
is the element being draggedtarget
is the element being dragged todragIndex
anddropIndex
are indexes in the VM models (lists)
If you need more advanced control over models (such as filtering, conditions etc.) you can use watchers on these models and then create derived models in response, perhaps dispatching local model state to a Vuex store. We recommend keeping the "raw" dragula models intact and in sync with the UI models/elements.
Each drake
is setup to delegate dragula events to the Vue event system (ie. $emit
) and sends events of the same name. This lets you define custom drag'n drop event handling as regular Vue event handlers.
A named service my-first
emits events such as drop
and my-first:drop
so you can choose to setup listeneres to for service specific events!
There are also two special events for when the underlying models are operated on: removeModel
and dropModel
. These also have service specific variants.
See Logging
See Model mechanics
See Customization
See Drake mechanics and configuration
See Drag Effects
See Time travel
Please help add more examples.
Please see the Wiki
Add an Rx Observable
or a watch
to your model (list) which triggers a sort
of a derived (ie. immutable) model whenever it is updated. You should then display the derived model in your view. Otherwise each sort operation would trigger a new sort.
MIT Kristian Mandrup 2016