👌 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.
- Works with Vue 2
- Way more flexible and powerful than original (Vue 1) plugin
- Removed concept of bags. Reference named drakes directly
- Vue2 demo app
See Changelog for more details.
We are currently implementing a model-manager feature. This will allow for hisory, undo/redo and custom model containers and operations.
A time-travel demo is under development. Please help implement and test this feature. Cheers!
The time travel is very close to fruition. The only missing part is handling it correctly on the VM side, all the infrastructure should be there. Just a matter of storing index/models for each action and then undo the actions by calling the Model Manager history actions and keep the local VM models correctly in sync.
npm
npm install vue2-dragula --save
yarn
yarn add vue2-dragula
Vue configuration
import Vue from 'vue'
import { Vue2Dragula } from 'vue2-dragula'
Vue.use(VueDragula, {
logging: {
service: true
}
});
<div class="wrapper">
<div class="container" v-dragula="colOne" drake="first">
<!-- with click -->
<div v-for="text in colOne" @click="onClick">{{text}} [click me]</div>
</div>
<div class="container" v-dragula="colTwo" drake="first">
<div v-for="text in colTwo">{{text}}</div>
</div>
</div>
You can access the global app service via Vue.$dragula.$service
or from within a component via this.$dragula.$service
.
You can also create named services for more fine grained control (more on this later)
Set dragula options
// ...
new Vue({
// ...
created () {
const service = Vue.$dragula.$service
service.options('my-drake', {
direction: 'vertical'
})
}
})
Returns the named drake
instance of the service.
For drake events
service.eventBus.$on('drop', (args) => {
console.log('drop: ' + args[0])
})
})
Event Name | Listener Arguments | Event Description |
---|---|---|
dropModel | drakeName, el, target, source, dropIndex | model was synced, dropIndex exposed |
removeModel | drakeName, el, container, removeIndex | model was synced, removeIndex exposed |
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 named service(s) via this.$dragula.createService
and initialise 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
The drake
event handlers have default mechanics for how to operate on the underlyng models. These can be customized as needed.
A common scenario is to have a tree of node objects, where each node has
a children
key. You'd want to be able to drag elements to modify the node tree stucture.
{
type: 'container'
children: [
{
type: 'form',
children: [
{
type: 'input'
as: 'text'
value: 'hello'
label: 'Your name'
},
{
type: 'input'
as: 'checkbox'
value: 'yes'
label: 'Feeling good?'
}
]
},
{
type: 'form',
children: [
]
}
]
}
In this example we should be able to move a form input specification object from one form container node to another. This is possible simply by
setting <template>
elements with v-dragula
directives to point to children[0].children
and children[1].children
respectively. We can use the rest of the node tree data to visualize the various different nodes. This could form the basis for a visual editor!
For fine-grained control on how nodes are added/removed from the various lists. Some lists might only allow that nodes added at the front or back, some might have validation/business rules etc.
The dragHandler
instance of the DragHandler
class encapsulates the states and logic of dragging and re-arranging the underlying models.
Sample code taken from handleModels
method of DragulaService
const dragHandler = this.createDragHandler({ ctx: this, name, drake })
drake.on('remove', dragHandler.remove)
drake.on('drag', dragHandler.drag)
drake.on('drop', dragHandler.drop)
Key model operation methods in DragHandler
- on
remove
drag action:removeModel
- on
drop
drag action:dropModelSame
andinsertModel
removeModel(el, container, source) {
this.sourceModel.splice(this.dragIndex, 1)
}
dropModelSame(dropElm, target, source) {
this.sourceModel.splice(this.dropIndex, 0, this.sourceModel.splice(this.dragIndex, 1)[0])
}
insertModel(targetModel, dropElmModel) {
targetModel.splice(this.dropIndex, 0, dropElmModel)
}
The DragHandler
class can be subclassed and the model operations customized as needed. You can pass a custom factory method createDragHandler
as a service option. Let's assume we have a MyDragHandler
class which extends DragHandler
and overrides key methods with custom logic. Now lets use it!
function createDragHandler({ctx, name, drake}) {
return new MyDragHandler({ ctx, name, drake })
}
export default {
props: [],
data() {
return {
//...
}
},
// setup services with drakes
created () {
this.$dragula.create({
name: 'myService',
createDragHandler,
drakes: {
third: true,
fourth: {
copy : true
}
}
})
}
}
Note that you can set a drake to true
as a convenience to configure it with default options. This is a shorthand for third: {}
. You can also pass an array of drake names, ie drakes: ['third', 'fourth']
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
and dropModel
are emitted as model items are moved around (using splice by default).
this.name, el, source, this.dragIndex
'my-first:removeModel': ({name, el, source, dragIndex, model}) => {
// ...
},
'my-first:dropModel': ({name, el, source, target, dropIndex, model}) => {
// ...
}
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.
You can pass a logging: true
as an option when initialising the plugin or when you create a new service.
Vue.use(VueDragula, {
// ...
logging: true
});
Fine grained logging
You can also specify more fine grained logging as follows:
Vue.use(VueDragula, {
// ...
logging: {
service: true,
dragHandler: true
}
});
The logging options are: plugin
, directive
, service
and dragHandler
Logging is essential in development mode!!
You can also subclass DragulaService
or create your own, then pass a createService
option for you install the plugin:
import { DragulaService } from 'vue-dragula'
class MyDragulaService extends DragulaService {
/// ...
}
function createService({name, eventBus, drakes}) {
return new MyDragulaService({
name,
eventBus,
drakes
})
}
Vue.use(VueDragula, { createService });
You can customize the event bus used via the createEventBus
option.
You could f.ex create an event bus factory method to always log events emitted if logging is turned on.
function createEventBus(Vue, options = {}) {
const eventBus = new Vue()
return {
$emit: function(event, args) {
if (options.logging) {
console.log('emit:', event, args)
}
eventBus.$emit(event, args)
}
})
}
Vue.use(VueDragula, { createEventBus });
In the directive bind
function we have the following core logic:
if (drake) {
drake.containers.push(container)
return
}
drake = dragula({
containers: [container]
})
service.add(name, drake)
If the drake already exists, ie. if (drake) { ... }
then we add the container directly into a pre-existing drake created in the created
lifecycle hook of the component. Otherwise it tries to register as a new named drake in the service drakes
map (Object).
Drake conflict warning
You can get a conflict if one or more drakes are added via directives, and the drakes have not been pre-configured in the VM. This conflict is caused by race conditions, as the directives are evaluated asynchronously for enhanced view performance!
Thanks to @Astray-git for making this clear
Note: @Astray-git is the original author of this plugin :)
Note: In the near future we will likely try to overcome this constraint, by always inserting the new container in an existing drake or simply overwriting.
Setup a service with one or more drakes ready for drag'n drop action
created () {
this.$dragula.create({
name: 'myService',
drakes: {
'first': {
copy: true
}
}
}).on({
// ... event handler map
})
}
You can also use the drakesFor
method on a registered service.
this.$dragula.drakesFor('myService', {
'first': {
copy: true
}
})
}
This ensures that the DragulaService
instance myService
is registered and contains one or more drakes which are ready to be populated by v-dragula
container elements.
<div class="wrapper">
<div class="container" v-dragula="colOne" service="myService" drake="first">
<!-- with click -->
<div v-for="text in colOne" @click="onClick">{{text}} [click me]</div>
</div>
<div class="container" v-dragula="colTwo" service="myService" drake="first">
<div v-for="text in colTwo">{{text}}</div>
</div>
</div>
If you simply specify the service name without a specific named drake configuration it will use the default
drake. A named service will always have a default
drake configuration.
<div class="container" v-dragula="colTwo" service="myService">
<div v-for="text in colTwo">{{text}}</div>
</div>
You can configure the default
drake simply using the drake
option on the service.
this.$dragula.create({
name: 'myService',
drake: {
}
})
When the v-dragula
directives are evaluated and bound to the component (via directive bind
method), they will each find a drake configuration of that name on the service and push their DOM container
to the list of drake.containers
.
if (drake) {
drake.containers.push(container)
return
}
If you need to add dragula containers and models programmatically, try something like this:
drake.models.push({
model: model,
container: container
})
Here the model
is a pointer to a list in the model data of your VM. The container is a DOM element which contains a list of elements that an be dragged and rearranged and their ordering reflected (mirrored) in the model.
To access and modify models and containers for a particular drake:
let drake = this.$dragula.service('my-list').find('third')
drake.models.push({
model: model,
container: container
})
drake.containers.push(container)
You will need a good understanding of the inner workings of Dragula in order to get this right ;) Feel free to help improve the API to make this easier!
Please see and try out the DragEffects
example of the demo app
// https://developer.mozilla.org/en/docs/Web/API/Element/classList
service.on({
accepts: ({name, el, target}) => {
return true
},
drag: ({el, container, service, drake}) => {
el.classList.remove('ex-moved')
},
drop: ({el, container}) => {
el.classList.add('ex-moved')
},
over: ({el, container}) => {
el.classList.add('ex-over')
},
out: ({el, container}) => {
el.classList.remove('ex-over')
}
})
Here name
is the drake name and drake
and server
are the drake and service instances (objects).
You can also subscribe to service specific events, here for drag
events from the service called my-first
'my-first:drag': ({el, container}) => {
el.classList.remove('ex-moved')
},
Sample effect styling with CSS fade-in transition effect
@keyframes fadeIn {
to {
opacity: 1;
}
}
.ex-moved {
animation: fadeIn .5s ease-in 1 forwards;
border: 2px solid yellow;
padding: 2px
}
.ex-over {
animation: fadeIn .5s ease-in 1 forwards;
border: 4px solid green;
padding: 2px
}
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