Aurelia and Drag/Drop Using Sortable

Last updated: September 22, 2015

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.

Purchase Aurelia for Real World Applications over at Leanpub now

 

Dwayne

 

One thought on “Aurelia and Drag/Drop Using Sortable

Leave a Reply

Your email address will not be published. Required fields are marked *