Awesome
Dragula for Vue2 via vue-dragula
A Vue.js demo app which demonstrates how to use Dragula with Vue 2 for drag and drop. Includes Time Travel demo (undo/redo) in Named service example.
Build Setup
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm start
# build for production with minification
npm run build
# run unit tests
npm run unit
# run e2e tests
npm run e2e
# run all tests
npm test
For detailed explanation on how things work, checkout the guide and docs for vue-loader.
TODO
- transitions
Transitions
We would very much like to add support for Vue transitions and transition groups as per discussions in this issue
Here is a jsFiddle Demo: single list with transitions
We might need to use Vue.set
to explicitly notify Vue that the underlying array has changed and thus activate the transition effect or change the way we update the Array in ModelManager
. Please experiment!
Development
To help improve the plugin, please do the following:
- fork vue2-dragula and clone it to local disk
- from within the root of
/vue2-dragula
runnpm link
to make a symbolic global link to this package - from the root of this demo app, run
npm link vue2-dragula
to install thevue2-dragula
module via the symbolic link
When you make changes to the plugin, make sure you run npm run build
in order to compile it to /dist
.
You can also set up a watcher to auto-build on every change.
Design
components
Home
brief overview of the examplesGlobalService
use of global app serviceNamedServices
named services withcopy: true
DragEffects
drag effects on a named serviceCustomModelManager
immutable model manager with time travel
router
The app is configured with a router which have the following components mounted:
/
:home
/global
:global
/named
:named
/effects
:effects
To add your own example page
Add a route in routes/index
and your example component in /components
.
Register the component in /components/index.js
and update the main navigation in App.vue
with a
link to your example route.
Bugs and issues
Please report bugs or issues
Using v-dragula directive
v-dragula
directive on an element must point to an underlying data model (Array
) in the VM.service
attribute specifies a registeredDragulaService
drake
attribute to use a specific named drake configuration registered on the service
Global app service example
If you don't specify a service the global application level dragula service $dragula.$service
will be used
<div class="wrapper">
<div class="container" v-dragula="colOne" drake="first">
<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>
Named services
DOM element containers can be configured to use specific named services:
<div class="wrapper">
<div class="container" v-dragula="colOne" service="first" drake="a">...</div>
<div class="container" v-dragula="colTwp" service="first" drake="b">...</div>
<div class="container" v-dragula="colTwo" service="first" drake="b">...</div>
<div class="container" v-dragula="stocks" service="second" drake="a">...</div>
</div>
Every service has a default
drake with default a dragula configuration.
You can use the default
drake by not setting the drake
attribute.
<div class="wrapper">
<div class="container" v-dragula="colOne" service="first">...</div>
<div class="container" v-dragula="colTwp" service="first">...</div>
<div class="container" v-dragula="colTwo" service="first" drake="b">...</div>
<div class="container" v-dragula="stocks" service="second">...</div>
</div>
Custom Model Manager with Time Travel
Time travel uses the following classes
ImmutableModelManager
TimeMachine
ActionManager
ImmutableModelManager
uses seamless-immutable which contains Immutable data structures for JavaScript which are backwards-compatible with normal JS Arrays and Objects.
Implements basic Time Travel with undo/redo back and forward in model history. Play with it and have fun!
The difference for the immutable collections is that methods which would mutate the collection, like push
, set
, unshift
or splice
instead return a new immutable collection.
Methods which return new arrays like slice
or concat
also return new immutable collections.
The local VM should maintain a history of transactions that can be undone.
An action consists of:
dragIndex
index in source modeldropIndex
index in target modelsourceModel
source list (or model manager that manages a list, ie. a model)targetModel
destination listtransitModel
item (or list) in transition from source to target
These params are also grouped for the insertAt
event:
models: {
source,
target,
transit
},
indexes: {
drag,
drop
},
elements: {
source, // container
target, // container
drop // element being dropped/inserted
}
The event handlers insertAt
and dropModel
can be used to manage the action history.
insertAt
is the best candidate, as it has access to all the action information.
'effects:insertAt': ({indexes, models, elements}) => {
},
'effects:dropModel': ({name, el, source, target, dragIndex, dropIndex, sourceModel}) => {
}
The ImmModelManager
contains all the history methods/tracking but we need to use this in the VM itself.
Both the sourceModel
and targetModel
have a history, so we can undo both and update the VM models to reflect it.
The VM/drake model references are encapsulated by the ImmModelManager
as modelRef
for both source
and target
models.
ImmModelManager
uses a TimeMachine
to manage history and handle time transitions.
The key method is timeTravel
method shown here, which sets the modelRef
via updateModelRef()
.
timeTravel
is used internally by both undo
and redo
. Note that updateModelRef
is also called internally by
insertAt
and removeAt
to ensure modelRef
is always in sync.
timeTravel (index) {
this.log('timeTravel to', index)
this.model = this.history[index]
this.updateModelRef()
return this
}
The actionManager
can be used to manage the done and undone actions on the containers (and models) of the VM.
created () {
// ...
this.actionManager = new actionManager({
logging: true
})
// ...
}
You can add an onUndo
and onRedo
handler as follows:
this.actionManager.onUndo((action) => {
let { models, indexes, elements } = action
log('onUndo', action, models, indexes, elements)
// ...
})
In the example we hook the actionManager
to some VM methods
methods: {
undo () {
this.actionManager.undo()
},
redo () {
this.actionManager.redo()
},
act (action) {
this.actionManager.act(action)
}
},
The insertAt
event handler performs a given action via the VM act
method.
'effects:insertAt': ({indexes, models, elements}) => {
this.act({
name,
models,
indexes
})
},
The template includes buttons to trigger undo
and redo
of those actions via the actionManager
.
<div class="actions">
<button @click="undo">undo</button>
<button @click="redo">redo</button>
<button @click="setRandom">generate</button>
</div>
Notice
If you check the log, you will see that for TimeMachine [...] set modelRef
it sets the VM model containers back to their original on undo
but the UI doesn't reflect this (Array pointer) update.
What to do to make the UI respond to this change!?
v-for="text in colOne"
needs to be forced to re-iterate somehow, see Vue2 list rendering.
and see sorting
"To work around this problem we need to add a unique identifier to our array items, and then bind this identifier to key property in our HTML."
Vue wraps an observed array’s mutation methods so they will also trigger view updates.
Vue implements some smart heuristics to maximize DOM element reuse, so replacing an array with another array containing overlapping objects is a very efficient operation.
See also list caveats
timeTravel (index) {
this.log('timeTravel to', index)
this.model = this.history[index]
// this.modelRef = mutable
// this.log('set modelRef', this.modelRef, this.model)
this.modelRef.splice(0, this.modelRef.length)
for (let item of this.model) {
this.modelRef.push(item)
}
return this
}
Let us know if you know/find a better, simpler or more efficient way to correctly trigger Vue2 to notice that the Array has been updated and update the VDOM + re-iterate the v-for
in the template/view.
You can experiment in setRandom
of the VM which uses the same strategy.
Dragula Service pre-configuration
Important Always pre-configure named services with drakes in the created
life cycle hook method of the VM.
created () {
let myService = this.$dragula.createService({
name: 'my-service',
drakes: {
first: {
copy: true,
}
}
})
let otherService = this.$dragula.createService({
name: 'other-service',
drake: {
// default drake config
}
})
myService.on({
drop: (el, container) => {
console.log('drop: ', el, container)
}
...
})
}
Styling
Add handles
.handle {
padding: 0 5px;
margin-right: 5px;
background-color: rgba(0, 0, 0, 0.4);
cursor: move;
}
Add a black border effect on :hover
over draggable child elements of a drake
container
[drake] >:hover {
border: 2px solid black
}
UX effects via event handlers
Add/Remove DOM element style classes as UX effects for drag'n drop events. Here using classList
service.on({
accepts: (drake, el, target) => {
log('accepts: ', el, target)
return true // target !== document.getElementById(left)
},
drag: (drake, el, container) => {
log('drag: ', 'el:', el, 'c:', container)
log('classList', el.classList)
el.classList.remove('ex-moved')
},
drop: (drake, el, container) => {
log('drop: ', el, container)
log('classList', el.classList)
el.classList.add('ex-moved')
},
over: (drake, el, container) => {
log('over: ', el, container)
log('classList', el.classList)
el.classList.add('ex-over')
},
out: (drake, el, container) => {
log('out: ', el, container)
log('classList', el.classList)
el.classList.remove('ex-over')
}
})
Sample effects styling
@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
}
Note that assets/styles.css
contains most of the styling used, primarily this part of interest:
.container .ex-moved {
background-color: #e74c3c;
}
.container.ex-over {
background-color: rgba(255, 255, 255, 0.3);
}
.handle {
padding: 0 5px;
margin-right: 5px;
background-color: rgba(0, 0, 0, 0.4);
cursor: move;
}
Tip: Please add more examples showcasing dynamic styling and transition effects to better visualize the drag and drop actions/events ;)
Configuring dragula options
Dragula includes loads of options you can use to fine tune the Dnd behaviour.
dragula(containers, {
isContainer: function (el) {
return false; // only elements in drake.containers will be taken into account
},
moves: function (el, source, handle, sibling) {
return true; // elements are always draggable by default
},
accepts: function (el, target, source, sibling) {
return true; // elements can be dropped in any of the `containers` by default
},
invalid: function (el, handle) {
return false; // don't prevent any drags from initiating by default
},
direction: 'vertical', // Y axis is considered when determining where an element would be dropped
copy: false, // elements are moved by default, not copied
copySortSource: false, // elements in copy-source containers can be reordered
revertOnSpill: false, // spilling will put the element back where it was dragged from, if this is true
removeOnSpill: false, // spilling will `.remove` the element, if this is true
mirrorContainer: document.body, // set the element that gets mirror elements appended
ignoreInputTextSelection: true // allows users to select input text, see details below
});
Let us know if this demo helps you and what you build with this example as your foundation.
Feel free to improve it or come with suggestions for new features etc :)
Enjoy!!!
License
MIT