Today we are going to be discussing how to use the Sortable Javascript library by RubaXa in our Aurelia applications.
Sortable is a highly configurable, no frills drag and drop library that has zero dependencies. So if you’re not using jQuery or just not a fan of libraries with dependencies, then this is the library for you. I also highly recommend Dragula but this article will be focusing on Sortable.
Getting Started
This article is going to assume you have an Aurelia application already up and running and using JSPM as your package manager. If you do not have Aurelia installed, the recommended and easiest way to get an Aurelia application up and running is to download the Skeleton Navigation application from Github here.
Open up a command line window (Terminal if you’re on a Mac or Command Prompt/PowerShell if you’re on Windows) and navigate to the root directory of your application and type: jspm install sortable=github:RubaXa/Sortable
What this will do is install the Sortable plugin from Github and namespace it under ‘sortable’ – if you take a look in your package.json file you will see the package there and in config.js you will see it has been added there as well.
Linked right at the bottom of this article is the source code and a working demo for everything detailed in this post. So if you’re not the follow along type, go check out the source code and have a play.
That was easy – Now that we have the Sortable plugin installed, we can then proceed to integrate it into our application. In this article we are going to be building a fictional application that inserts random images of cats and dogs into your application.
The Workflow
As always with any application, mapping out the workflow before we build anything is a good idea. An engineer doesn’t build a bridge without the plans for building it and you don’t build a house without a blueprint.
- User has the choice of multiple elements.
- Those elements cannot be sorted, but they can be dragged into a special area on the page.
- When the user drags an item and drops it, we want to cancel the event and handle things ourself.
- Depending on the type of item dropped into the special area, we will fetch a random image of a cat or dog.
- Our Sortable needs to play nicely with the Aurelia
repeat.for
atrribute, including the ability to resort the array items - Dropped images will be sortable
- Dropped items can be removed from the page
Let’s Build
Now that we have our blueprint, lets build this thing.
Creating a View and ViewModel
Using default Aurelia conventions, we are going to be using a named View and ViewModel pair. By default a ViewModel is assumed to have a View aka template of the same name, only with a .html
file extension instead.
Lets use the name DragDrop for our application View and ViewFiles. Inside of the “src” directory create two new files: dragdrop.js
and dragdrop.html
.
Now that we have our two files where all of the action takes place, lets populate them.
dragdrop.html
The HTML template is going to be quite simple, so lets populate that first. Here we specify an area where elements are dragged from and dragged to. Aptly named, “drag-target” and “drag-source” – items from the “source” area are dragged into the “target” area.
<template>
<div id="wrapper">
<div id="drag-target">
<div class="dragged-element" repeat.for="item of droppedItems">
<div class="img-wrap">
<img src="${item.src}">
${item.type === 'cat' ? 'Cat' : 'Dog'}
</div>
</div>
</div>
<div id="drag-source">
< a href="javascript:void(0);" data-type="cat">Random Cat< /a>
< a href="javascript:void(0);" data-type="dog">Random Dog< /a>
</div>
</div>
</template>
dragdrop.js
This is where we handle our drag and drop logic for our application. There is going to be quite a bit of code, so if you just end up copying and pasting this, I understand.
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import sortable from 'sortable';
@inject(EventAggregator)
export class DragDrop {
droppedItems = [];
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
}
/**
* Attached
* Called when view is attached
*
*/
attached() {
this.setupSource(document.getElementById('drag-source'), false, {
name: 'catsAndDogs',
pull: 'clone',
put: false
});
this.setupTarget(document.getElementById('drag-target'), '.dragged-element', true, 'catsAndDogs');
this.eventListeners();
}
/**
* Event Listeners
* This is where event listeners for drag/drop events are registered
*
*/
eventListeners() {
// Event triggered when item is added
this.eventAggregator.subscribe('dragTarget.onAdd', evt => {
let src = evt.from;
let dest = evt.to;
let item = evt.item;
// When actual dragged item is dropped, we remove it and handle
// updating the array for our repeater ourselves
evt.item.parentElement.removeChild(evt.item);
// Dragging widget into new page
if (item.dataset.type) {
let animalType = item.dataset.type;
let itemInstance = {};
if (animalType === 'cat') {
itemInstance.type = 'cat';
itemInstance.src = 'http://thecatapi.com/api/images/get?format=src&type=jpg';
} else {
itemInstance.type = 'dog';
itemInstance.src = 'http://loremflickr.com/640/480/dog';
}
this.droppedItems.splice(evt.newIndex - 1, 0, itemInstance);
}
});
// Events for when sorting takes place, we need to update the array to let
// Aurelia know that changes have taken place and our repeater is up-to-date
this.eventAggregator.subscribe('dragTarget.onUpdate', evt => {
// The item being dragged
let el = evt.item;
// Old index position of item
let oldIndex = evt.oldIndex;
// New index position of item
let newIndex = evt.newIndex;
// If item isn't being dropped into its original place
if (newIndex != oldIndex) {
swapArrayElements(this.droppedItems, newIndex, oldIndex);
evt.item.parentElement.removeChild(evt.item);
}
});
}
/**
* Setup Source
* Handles setting the drag source
*
* @param el (string)
* @param sort (boolean)
* @param group (object)
*/
setupSource(el, sort = false, group = {}) {
new sortable(el, {
sort: sort,
group: group,
onStart: evt => {
this.eventAggregator.publish('dragSource.onStart', evt);
},
onEnd: evt => {
this.eventAggregator.publish('dragSource.onEnd', evt);
}
});
}
/**
* Setup Target
* Handles setting the drag target destination for dragged items
*
* @param el (string)
* @param draggable (string)
* @param sort (boolean)
* @param group (object)
*/
setupTarget(el, draggable = '.element', sort = true, group = 'somegroup') {
new sortable(el, {
draggable: draggable,
sort: sort,
group: group,
onAdd: evt => {
this.eventAggregator.publish('dragTarget.onAdd', evt);
},
onUpdate: evt => {
this.eventAggregator.publish('dragTarget.onUpdate', evt);
}
});
}
}
function swapArrayElements(theArray, a, b) {
var temp = theArray[a];
theArray[a] = theArray[b];
theArray[b] = temp;
}
See it in action
A working demo can be seen here. The source code can be seen here on Github.
Conclusion
As you can see integrating third party libraries within Aurelia is easy. The bulk majority of our code is actually getting the sorting functionality to place nicely with our Aurelia repeater.
We cancel the original drag event, parse the dragged item for details and then insert our new item manually. We also handle sorting of the items by swapping the positions of the dragged items within the droppedItems
class property array.
Demo doesn’t seem to work anymore?