Ember.js and HTML5 Drag and Drop

September 25, 2014 · 4 mins read
Engineering
Photo by rawpixel on Unsplash

It’s fairly trivial to add a ‘drag and drop’ interface to Ember.js with html5, without the need for external libraries, although the caveat is that mobile browsers don’t (yet) support html5 drag and drop.

The basic premise is to create 2 components:

  • a draggable-dropzone component, that handles when an item is dropped into it (e.g. a shopping cart)
  • a draggable-item component, that sets up the data transfer into the dropzone

Using transclusion, we can then easily add our ‘drag and drop’ functionality into our Ember application with different UI.

demo (with extra bells and whistles)

Code: JSBin demo

via Gfycat

draggable-dropzone

Let’s first define our draggable-dropzone component:

import Ember from 'ember';

var { set } = Ember;

export default Ember.Component.extend({
  classNames        : [ 'draggableDropzone' ],
  classNameBindings : [ 'dragClass' ],
  dragClass         : 'deactivated',

  dragLeave(event) {
    event.preventDefault();
    set(this, 'dragClass', 'deactivated');
  },

  dragOver(event) {
    event.preventDefault();
    set(this, 'dragClass', 'activated');
  },

  drop(event) {
    var data = event.dataTransfer.getData('text/data');
    this.sendAction('dropped', data);

    set(this, 'dragClass', 'deactivated');
  }
});
{{yield}}

Ember includes built-in events for the html5 drag and drop API, so we can make use of them right out of the box. We also set up a classNamebinding to provide visual cues to our user that the element is a dropzone for dragging things into.

draggable-item

Next, we define the component for setting up items to be draggable:

import Ember from 'ember';

var { get } = Ember;

export default Ember.Component.extend({
  classNames        : [ 'draggableItem' ],
  attributeBindings : [ 'draggable' ],
  draggable         : 'true',

  dragStart(event) {
    return event.dataTransfer.setData('text/data', get(this, 'content'));
  }
});
{{yield}}

draggable is, as you’d expect, the attribute that makes a DOM node draggable. We simply set the attribute of the component, and handle the dragStart event to get the content of the component. Unfortunately, the HTML5 API does not allow for draggables to transfer JavaScript, so we’ll have to perform a workaround.

Using the components in our application

One thing to note is that we can’t transfer actual objects or records through the API, so when we include the component into our template, we have to set its content to the record’s ID.

Here’s a simple example of an interface for adding users to a new ‘team’ record we want to save:

<div class="selected-users">
  {{#draggable-dropzone dropped="addUser"}}
    <ul class="selected-users-list">
      {{#each user in selectedUsers}}
        <li>{{user.fullName}}</li>
      {{/each}}
    </ul>
  {{/draggable-dropzone}}
</div>

<div class="available-users">
  {{#each user in users}}
    {{#draggable-item content=user.id}}
      <span>{{user.fullName}}</span>
    {{/draggable-item}}
  {{/each}}
</div>

If you recall, we sent an action on drop in the draggable-dropzone component:

drop(event) {
  // ...
  this.sendAction('dropped', data);
}

Now we simply need to define an appropriate action on our controller to handle the drag and drop:

import Ember from 'ember';

var { computed, get } = Ember;

export default Ember.ArrayController.extend({
  selectedUsers        : Ember.A([]),
  remainingUsers       : computed.setDiff('model', 'selectedUsers'),
  remainingUsersLength : computed.alias('remainingUsers.length'),

  actions: {
    addUser(userId) {
      var selectedUsers = get(this, 'selectedUsers');
      var user          = get(this, 'model').findBy('id', parseInt(userId));

      if (!selectedUsers.contains(user)) {
        return selectedUsers.pushObject(user);
      }
    },

    addAllUsers() {
      var remainingUsers = get(this, 'remainingUsers')
      return get(this, 'selectedUsers').pushObjects(remainingUsers);
    },

    removeUser(user) {
      return get(this, 'selectedUsers').removeObject(user);
    },

    removeAllUsers() {
      return get(this, 'selectedUsers').clear();
    }
  }
});

In our Controller, we simply find the appropriate user record from the store (using the userId we get from the draggable-item component), and then push it into an array called selectedUsers. We can then do whatever we want with this array (e.g. create a new team with the selected users).

Some basic styling:

$default-margin : 20px;
$gray-light     : #e1e1e1;
$gray-medium    : #aaa;
$green          : #2ecc71;
$white          : #fff;
$black          : #111;

.draggableDropzone {
   display: block;
   border: 3px dashed $gray-medium;
   padding: $default-margin / 2;
   width: 100%;
   min-height: $default-margin * 2.5;
   color: $gray-medium;
   margin-bottom: $default-margin / 2;
   &.activated {
     border-color: $green;
   }
   &.deactivated {
     border-color: $gray-light;
   }
}

.draggableItem[draggable=true] {
   display: inline-block;
   min-width: $default-margin;
   min-height: $default-margin;
   background: $gray-light;
   padding: $default-margin / 4 $default-margin / 2;
   margin: $default-margin / 4;
   -moz-user-select: none;
   -khtml-user-drag: element;
   cursor: move;
   &:hover {
     background-color: $gray-medium;
   }
}

Over at doceo, we’ve added more functionality to the interface, including typeahead search, and other useful controls (add all users, removing all users, removing a single user). This basic setup is sufficient enough to handle most use cases for a drag and drop UI, and having transclusion (yield) lets us define different designs for the same drag and drop functionality.

Special thanks to @itscatkins for pairing with me for this one.

Discuss on Twitter · Edit this post on GitHub

Written by Lauren Tan who lives and works in the Bay Area building useful things. You should follow her on Twitter