Making a draggable Ember component

HTML5 makes it really easy to create elements that can be both dragged and dropped. This is a really powerful API to add rich functionality to your application. In this post we'll use this API to make a simple Ember component that can be repositioned on the screen by dragging it with the mouse.

The HTML5 drag and drop API allows you to add rich interactivity to your applications. It is usually used to allow files to be dropped onto upload forms, removing the need for tedious file picker dialogs. You can also use it to allow users to interactively sort lists of elements or allow the user to manually move elements between columns.

To get a sense how this API works, we’re going to start small and make a simple Ember component that can be dragged and respositioned on the screen. For example, this is useful if you needed to create a photoshop-esque palette that contains tools the user can drag around the screen.

!GIF of a draggable box being moved around](https://cdn.timmyomahony.com/assets/drag-and-drop.f1595777530.gif)

To make this work, we first need to understand the basics of the drag and drop API. When we are developing a component that uses the drag and drop API, there are 3 parts we need to consider:

  • a source element that is being dragged somewhere
  • a destination element that the source element is being dropped onto
  • an approach for these to elements to communicate

These concepts are represented in the API by a number of browser events. So to designate the source and destination elements, we need to assign them both a number of event handlers and attributes.

The source element (in our case, the black box) that is being dragged needs to have the attribute draggable=true assigned as well as a dragStart event handler that collects any information that we want to pass to our destination via the communication interface e.dataTransfer.setData(key, value).

The destination element (in our case, the entire window) that the source will be dropped onto needs to define a drop and dragEnd event handler. The dragEnd event handler will receive the information from our source element via the communicate interface and reposition the source element accordingly.

You can see how this work in the following Ember component:

component.js
/* global document, $ */
 
import Ember from "ember";
 
export default Ember.Component.extend({
  tagName: "div",
  classNames: ["drag-box"],
  attributeBindings: ["draggable"],
  draggable: true,
  visible: true,
  x: 0,
  y: 0,
  // When out positioning attributes change, update
  // the underlying CSS for this component
  positionChanged: Ember.observer("x", "y", function () {
    this.$().css({
      left: `${this.get("x")}px`,
      top: `${this.get("y")}px`,
    });
  }),
  // This event handler is automatically fired on the component's
  // element (the black box) when the mouse is pressed down
  // and first dragged. It collects information on the position
  // where the component was clicked. This is passed to the
  // destinations dragend event handler via the communication
  // interface
  dragStart(e) {
    var x = parseInt(this.$().css("left")) - e.originalEvent.clientX;
    var y = parseInt(this.$().css("top")) - e.originalEvent.clientY;
    e.originalEvent.dataTransfer.setData("position", `${x},${y}`);
  },
  // We are  binding the drop and dragend event handlers to the
  // document element, not the component itself. When the mouse
  // button is released, the drop event handler is fired on the
  // document to figure out where the component should be
  // moved by getting information passed via the communication
  // interface (which tracks where in our component element
  // the drag was initiated)
  windowDrop(e) {
    var xy = e.dataTransfer.getData("position").split(",");
    this.setProperties({
      x: `${e.originalEvent.clientX + parseInt(xy[0])}`,
      y: `${e.originalEvent.clientY + parseInt(xy[1])}`,
    });
  },
  windowDragOver(e) {
    e.originalEvent.preventDefault();
  },
  didRender() {
    var self = this;
    $(document)
      .on("drop", $.proxy(self.windowDrop, self))
      .on("dragover", $.proxy(self.windowDragOver, self));
  },
  // Remember to unbind the document event handlers
  didDestroyElement() {
    $(document).off("drop").off("dragover");
  },
});

Remember, to see this component in action and to get the rest of the relative code, you can check out the Ember Twiddle, or look at the Gist.

Get the code

👋 You can see this in action on Ember Twiddle, or look at the code on Github