POST DIRECTORY
Category software development

I’ve been doing a lot of work on an internal planning and organization tool for HCW. It’s an Ember application that among other things offers a simple kanban board. As you would expect of any kanban board, the interface is drag and drop heavy: cards can be sorted within their own lanes, cards can be moved between lanes, checklist items on cards can be sorted, uploaded images and assets on cards can be rearranged, and we’re sure to add more as we go.

What we had and what was missing

I started out with a simple set of components that wrap draggable elements and dropzone targets for those elements. The components converted drop events into actions to be handled somewhere up the Ember ancestor chain. These components were used to implement card management in the kanban lanes, and while they worked ok for that purpose there were a few shortcomings.

The biggest problem was poor visual feedback when dragging an item around. Making it clear where an item will end up is surprisingly difficult. My approach was to add a vaguely card shaped, solid colored rectangle where the item would end up. It was functional but not very attractive, and didn’t really serve as a preview of the post-drop state.

The next shortcoming was a lack of testing on mobile devices. Our primary target for the application is desktop browsers, but eventually we need to make sure the kanban board works on mobile browsers too.

Another big inconvenience was the lack of auto scrolling when items were dragged outside the window boundaries. Dragging a card from the bottom of a long lane to the top required either some nimble drag and scroll trackpad/mouse manipulation, or a bunch of sequential drag and scroll steps.

Finally, the components would need to be extended to support multiple drag and drop interfaces on one page. This would require the components to support drop exclusion rules: cards should not be dropped onto checklists, checklist items should not be dropped onto kanban lanes, etc.

Since we were looking at a healthy amount of work extending these components to support our evolving needs, it was time to consider alternatives.

Enter Dragula

I heard about Dragula right around this time, and after taking a look at some demos and reading over the documentation it seemed to fit our needs. It has really nice visual feedback in the form of a preview ‘shadow’ where the item will be dropped. It supports desktop and mobile browsers. It allows management of independent sets of exclusive dropzones. On top of all that it’s lightweight and dependency free.

GitHub: https://github.com/bevacqua/dragula

Demo: http://bevacqua.github.io/dragula/

Dragula Basics

The Dragula library is pretty easy to work with, it comes with sane defaults and the API is small. The central object in dragula is the “drake”. A drake manages drag and drop operations for a set of registered containers.

The child elements of these containers can be moved within or between them. During drag and drop operations the drake can handle a variety of events, but the main one is “drop”.

When an item is dropped, the drake runs a callback and passes it the relevant DOM elements:

el: the dragged element

target: the container element el was dropped into

source: the container element el was dragged from

sibling: the child element after where el was dropped (or null)

While this describes the basics when using Dragula’s default behaviors, there is a lot of flexibility in configuring the specifics if you need it.

Simple Proof of Concept Component

The next step is to build out a simple proof of concept component to verify it can do what we need. At this stage I just need a thin wrapper component around the drake to use for experimenting and answering the basic questions:

Will it support our known drag and drop use cases?

Will it solve the other deficiencies identified in the current implementation?

Install Dragula

(At this time, Dragula is at version 3.5.1)

Dragula is simple to install using npm or bower, I use bower:

bower install dragula --save

Then I add the assets to the ember-cli build file (ember-cli-build.js):

. . .
// dragula - drag and drop
app.import('bower_components/dragula/dist/dragula.js');
app.import('bower_components/dragula/dist/dragula.css');
. . .

Wrangling the Drake

Taking baby steps, I start with a component that sets up and tears down the drake. The setup happens in the willInsertElement hook in the component, and teardown happens in the willDestroyElement hook. I also add a pass-through template for the component.

// simple-drag-and-drop/component.js
import Ember from 'ember';

export default Ember.Component.extend({
  willInsertElement: () => {
    let drake = window.dragula();
    this.set("drake", drake);
  },
  willDestroyElement: () => {
    this.get("drake").destroy();
    this.set("drake", null);
  }
});
<!-- simple-drag-and-drop/template.hbs -->
{{yield}}

Next I add container registration based on a css selector set on the component tag. The selector is applied to the body of the component to select container elements for the drake to manage. Dragula options can be overridden on the component tag as well.

// simple-drag-and-drop/component.js
import Ember from 'ember';

export default Ember.Component.extend({
  willInsertElement: () => {
    let drake = window.dragula(this.dragulaContainers(), this.dragulaOptions());
    this.set("drake", drake);
  },
  willDestroyElement: () => {
    this.get("drake").destroy();
    this.set("drake", null);
  },
  dragulaContainers: () => {
    return this.$(this.containerSelector).get();
  },
  dragulaOptions: () => {
    return this.options || {};
  }
});

Finally I add a drop handler to the drake that causes the component to send an action up the Ember ancestor chain. The action from the ancestor’s context can be set on the “dropped” attribute of the component.

// simple-drag-and-drop/component.js
import Ember from 'ember';

export default Ember.Component.extend({
  willInsertElement: function() {
    let drake = window.dragula(this.dragulaContainers(), this.dragulaOptions());
    drake.on("drop", (el, target, source, sibling) => {
      this.sendAction("dropped", el, target, source, sibling);
    });
    this.set("drake", drake);
  },
  willDestroyElement: function() {
    this.get("drake").destroy();
    this.set("drake", null);
  },
  dragulaContainers: function() {
    return this.$(this.containerSelector).get();
  },
  dragulaOptions: function() {
    return this.options || {};
  }
});

Going for a Ride

With a simple component providing a thin wrapper for Dragula, I can test out our known use cases and verify that it satisfies our requirements. In our planning app, I was able to test this out using real models and components and logging the dropped callback output to the console.

For this post I created a few static examples in Ember Twiddle so you can get a feel for using the component and see Dragula in action. The different divs have titles to indicate which container or drake they belong to, just hover to see them. The browser console will show drake setup and teardown as well as the arguments passed to the “dropped” action handler.

Enter Dragula Twiddle

The first example has one drake managing one container. This would be like a checklist with draggable items.

The second example has one drake managing multiple containers, like the lanes in a kanban board. You can freely drag items within and between these containers.

The third example has multiple drakes managing distinct sets of containers. This supports having multiple checklists, kanban lanes, and more on the same page. The left column is managed by the first drake and has two containers. The right column is managed by the second drake and has three containers. You can freely drag an item within and between containers managed by it’s own drake, but you cannot drag items between containers managed by different drakes.

Proof of Concept Results

This simple proof of concept component was a qualified success. Installing and understanding the Dragula library was easy, and getting the simple component working only took a few hours. In addition, the component handled all of our basic use cases easily.

Playing with different variations on the wrapper component also generated a lot of new ideas for implementing better drag and drop components for Ember, regardless of the underlying library being used.

Concerns

Despite the successes, working with the component revealed a flaw. Dragula currently does not handle auto scrolling the browser window during dragging. There is an active issue for this and a PR in process to fix it.

https://github.com/bevacqua/dragula/issues/121

Since our software is pre-alpha I’m content to wait for this feature to drop, and allowing a sub-optimal user experience in the short term.

Next Steps

Based on my experience so far, I plan on moving forward with Dragula. In the next post I’ll explore ways to more tightly integrate the library with Ember.

I’ll figure out how to handle containers that are added or removed dynamically and register or un-register them with the drake. This will support features like adding or deleting kanban lanes.

I’ll figure out a nice way to bridge the DOM elements Dragula uses in callbacks with Ember models. This would dramatically simplify controllers that need to sync the DOM with the Ember data model.

And just to spice things up I’ll include a plot twist!

''