How to do Drag and Drop in Ember.js… with Mixins

Implementing drag and drop in Ember has given me headaches, and after finally breaking down and searching hard on the internet, I found a Stack Overflow post that had a pretty good solution.

Unfortunately, I’m still not very good at Ember.  I don’t think it’s an IQ thing.. I think it’s more of an Ember is a different beast thing.  Ember does way more out of the box than I would ever expect, and I end up fighting it a lot.  And losing.  Anyway.

Here’s the mixin I created (i.e. mostly stole from a Stack Overflow answer) for the app I’m working on:

DnD = Ember.Namespace.create();

DnD.cancel = function(event) {
  event.preventDefault();
  return false;
};

DnD.dragStartHandler = function(event) {
  //record the id, alt tag of the element being dragged.
  event.dataTransfer.setData("text/plain", JSON.stringify({alt: this.element.alt, id: this.element.id}));
}

DnD.Draggable = Ember.Mixin.create({
  attributeBindings: 'draggable',
  draggable: 'true',
  dragStart: DnD.dragStartHandler
});

DnD.Droppable = Ember.Mixin.create({
  dragEnter: DnD.cancel,
  dragOver: DnD.cancel,
  dragLeave: DnD.cancel,
  drop: function(event) {
    event.preventDefault();
    var data = event.dataTransfer.getData("text/plain");
    console.log("data from droppable: " + JSON.parse(data));
    console.log("event.target: " + event.target.id);
    this.sendAction("dropped", event.target.id, JSON.parse(data));
  }
});

Let’s walk through it piece by piece.

First, I create a namespace.  Here’s the documentation if you’re curious: http://emberjs.com/api/classes/Ember.Namespace.html.

It just makes a nice pretty little object I can dangle things off of.   In truth, I have no idea why you need to use it.  Talk to the Ember faeries.  I’m sure they can help you demystify Ember’s magic.

The cancel function takes care of the ondragenter, ondragover, and ondragleave handler for my use case.  The user doesn’t need any kind of indication that they are over a drop area and nothing happens as a result of being over a drop area.  If you want something to happen in that function, you will need to specify it in between the event.preventDefault() and return false lines, as both of those are required to cancel the default action for drag and drop.

DnD.Draggable is the name of the mixin I am using for anything I make draggable (in this case, a Magic: the Gathering card image built out using an Ember component).  Here, I set draggable to true using an attributeBinding.  dragStart is equivalent to ondragstart=”[some function](event)” on the draggable object in html code, only that would be a pain to write over and over again.  In my implementation of the handler, I’m sending both the alt attribute and the id of the target using dataTransfer by creating a JSON object and stringifying it.

DnD.Draggable = Ember.Mixin.create({
  attributeBindings: 'draggable',
  draggable: 'true',
  dragStart: DnD.dragStartHandler
});

The other mixin I create  is dubbed DnD.Droppable.  Objects I create with this mixin will be valid drop zones for components with the DnD.Draggable mixin.  The dragEnter event is equivalent to the ondragenter event in HTML 5.  Similar are true for dragOver and dragLeave.  And again, since I do not need to do anything when these events trigger aside from preventing the default action, my DnD.cancel function suffices for my use case.

The drop function is going to be the hardest piece to absorb if you are new to Ember, or to drag and drop.  The first thing I do is retrieve the data being transferred from the object I am dropping using dataTransfer.getData.  Once that’s done, I send the action on using sendAction. The console.log calls were there simply to test the code.

drop: function(event) {
  event.preventDefault();
  var data = event.dataTransfer.getData("text/plain");
  console.log("data from droppable: " + JSON.parse(data));
  console.log("event.target: " + event.target.id);
  this.sendAction("dropped", event.target.id, JSON.parse(data));
}

There are a few reasons for doing this.  The first reason is you aren’t supposed to handle actions on your components in Ember.  It makes Ember angry… and Ember doesn’t like being angry…
Your components are supposed to handle the low-level stuff (e.g. “Hey! a click happened” or “Hey! the user submitted a form” or “Hey! your house is on fire”), and then pass the data to the controllers for processing. Keeps everything nice and neat.

The second reason I do this is so that I can specify what I want to do when a card is dropped on a particular zone.  This is super-handy in my use case, since I will need to do different things depending on where the card is dropped.  Generally speaking, it’s straightforward, but there are a few exceptions that I would like to do something a bit different with.  You may be wondering where my action is going and what it’s doing.  First, we will have to look at the controller using a mixin, and its handlebars implementation.

This is my game zone component and card image component. Notice the DnD.Droppable after Ember.Component.extend below? All the mixin attributes and other stuff are added to this component. Now, any instances of GameZoneComponent I create are a valid dropzone for my CardImgComponent, which have the DnD.Draggable mixin added in the same fashion:

Areas.GameZoneComponent= Ember.Component.extend(DnD.Droppable, {
  classNames: ['zone', 'display-zone'],
  classNameBindings: ['display-zone', 'player', 'location', 'id'],
  displayZone: true,
  player: "p1",
  location: "somelocation"
});
Areas.CardImgComponent = Ember.Component.extend(DnD.Draggable, {
  classNames: ['card'],
  classNameBindings: ['player', 'location'],
  player: "p1",
  tagName: 'img',
  attributeBindings: ['src', 'alt']
});

Now, since I am sending an action via the “dropped” attribute on the GameZoneComponent, each time I use the component, I will need to specify an action, as in this one:

{{game-zone cards=lib1 id="lib1" class="zone display-zone p1 library hidden" dropped="moveCard"}}

moveCard is the name of an action on my GameController and it looks like this:

moveCard: function(target, card) {
  var mycard = this.model.filterBy('domid', card.id);
  var domcard = document.getElementById(card.id);
  if(target.indexOf("card") == -1) {
    mycard.objectAt(0).set('zone', target);
    document.getElementById(target).appendChild(domcard);
    this.get('model').save();
  }
}

This is really where all the magic happens.  I update the data model based on the drop, and then move the object to where it was dropped.  There is probably a more semantically correct way to update the DOM when the underlying data model changes, and I’m guessing it involves observers.  This works, so I’m keeping it as-is for now… unless one of you guys want to share a solution to this problem?

Comments and questions are always welcome.  Hope this helped.  Oh, and before I get flamed for not giving credit where credit is due, here is the Stack Overflow answer that was mostly responsible for this: http://stackoverflow.com/questions/10739322/dragdrop-with-ember-js and was written by pangratz based on a post by Remy Sharp here.

I’ll leave you with another little tidbit of code. These are my game zone component and card image component templates. Ember magic ties them to the code I showed you earlier. GameZoneComponent will look for a handlebars template with the name: “components/game-zone,” and similar is true for CardImgComponent: “components/card-img.”

<!-- 	4. Card-img Component  -->
</script>
<script type="text/x-handlebars" data-template-name="components/card-img">
    <img {{action "tap" on='click'}}>
</script>

<!--	5. Game-zone Component  -->
<script type="text/x-handlebars" data-template-name="components/game-zone">
	{{#each card in cards}}
		{{card-img card=card draggable="true" class="card" src=card.img elementId=card.domid alt=card.name}}
	{{/each}}
</script>

I’d be willing to make a JSfiddle so you can dig through the code yourself and see it in action. Just let me know in the comments and I’ll toss a link in there.