Write a post

AngularJS Tutorial: How I Built the Drag-and-Drop Directives on Plunker

Published Apr 02, 2015Last updated Jun 11, 2017
AngularJS Tutorial: How I Built the Drag-and-Drop Directives on Plunker

In this article, Geoff Goodman, the creator of Plunker, shares with us how he personally went about implementing a custom drag and drop solution for Plunker with AngularJS, and his experience should help you figure out what you should do for your project regardless of the language or framework. It will also go over what sort of alternatives there are, and why they weren’t used for Plunker. Finally, this post will also cover the design and implementation decisions in the creation of Plunker. The post is based on the Codementor Office Hours hosted by Geoff.

Angular & Plunker

Codementor Plunker AngularJS Office Hours

What is Plunker?

Plunker is an in-browser code editor. The genesis behind this site is that I had a lot of idea for things I wanted to create but found that there were a number of barriers between having an idea and implementing that idea. I was using JSFiddle at the time and found that a number of frustrations with that website prevented me from implementing these ideas so I decided to make my own. But clearly a code editor, any modern code editor for that matter, is pretty useless is you can’t drag things around, certainly when you’re dealing with multiple files, and in the case of Plunker, one of the main features is that there are multiple files.

Step 1: Define the Goal

Here’s a little demo of me trying to drag files around with a cheesy animation.

Angular & Plunker Office Hours: Demo 1

So, fail. As you can see it doesn’t allow you to do this sort of intuitive behaviors you’d expect from an interface like that. I knew that I wanted to take this to the next level in the next version of Plunker, so here are a couple quick animations of what I was hoping to accomplish:

Angular & Plunker Office Hours: Demo 2

You can see here I’m dragging script.js from the lib folder to the project folder, and as you can see, if I can repeat that, it moves. So that’s one behavior I wanted to enable but then, boom, that sort of pane splitting is the second behavior I wanted to enable. But what kind of existing libraries would enable that sort of behavior? I tried looking around to see what was available and found that there was nothing much that could suit my needs.

Step 2: Research Solutions

So what I did do, is I kind of scanned the internet to see what was out there. I was working Angular, so I decided to target libraries that targeted Angular that would certainly simplify my work. So I found Angular drag-drop, which is a great little wrapper around jQuery UI. That would’ve been fine in the existing Plunker where I am using jQuery (I am using jQuery UI), but in the next version of Plunker that I’m making, I’ve decided not to use those two libraries. So just to implement drag and drop, I wasn’t willing to need to include such heavy libraries just to enable one small feature.

I also looked at NG Sortable, which in my opinion is a pretty cool library. It’s very good for sorting lists, reordering lists, moving items from one list to the other, but it was not adapted to that sort of pane splitting behavior I showed in that video earlier.

I also saw this other Drag-and-Drop solution, which is quite close to what I’ve done, but it wouldn’t have been able to allow me to do what I was looking for.

Step 3: Find out What is Needed

After I realized I couldn’t use something else someone had already built and is good, I brainstormed how I would go about this.

I know that I’ve got things that I wanted to drag, I know that there are only certain places to drop stuff, and I know that only certain things can be dropped in certain places.

If you saw earlier in that video, you’ll notice that I can drag files into folders, and that makes sense intuitively. We know that in my file system we can drag files and we can drag them into folders, but we can’t drag folders into files, so there’s a sort of logic that need to be allowed for the system to determine.

The things I needed to figure out for a drag-and-drop function in Plunker were:

  1. What is being dragged
  2. Where you’re trying to drop the file, and establish whether or not that’s a valid combination.
  3. There’s another type of drag-in. For example, if you saw earlier I was dragging files from the sidebar into the editor pane and it was splitting those panes, so it would enable that. I cannot drag a folder into the editor pane because the editor pane is not adapted to editing folders.

So those three points pretty much make up my needs for this tool. After figuring what I needed, I went about thinking about how I might make this in Angular, how I might convert this to code.

In Angular, there is a concept called a directive. A directive is sort of like a custom element or customer attribute in an HTML document that allows you to attach behavior to part of your document. They’re not the easiest thing to learn, and they’re not the easiest thing to understand when you’re looking at them. However, directives can let you do some pretty crazy things and reuse those things easily.

As I did a survey of what was available, I realized that there was this nice HTML5 spec for dragging and dropping, which allows you to leverage the browser’s internal capabilities to clone DOM elements visually to give you a visual representation of what you’re dragging. It also gives you visual cues of whether you are able to drop it somewhere, whether the drop is going to be a copy, whether it’s going to be a move, etc.

Thanks to finding the HTML5 Spec, I realized I could dramatically simplify what I was going to do, since now I don’t have to worry about having to clone DOM objects and create little phantom DOM objects to move around. I realized that I could leverage the power of Angular directives and this existing spec to make my work a lot easier. However, the HTML5 API was also a little bit of a counter-intuitive, since if an element should accept drops in a drag-and-drop system, you actually have to cancel or prevent the default action or drag into and drag over events. Otherwise the browsers will take their default action and simply not react to this sort of event.

So you would think that preventDefault() might be the opposite of what you want to do, but in reality, to take the advantage to leverage this API, you need to prevent the default action to opt into this alternative action where the system would accept drop events.

There was also one major issue I had to face. Although the Internet Explorer pioneered this drag and drop API, they’ve become a little bit of the black sheep now, as the standard/spec is moved beyond what they do. IE only allows you to specify items that are being dragged as text or as URLs.

However, in Plunker, what I’m actually dragging around are files, folders, or panes. This means you can drag a file into a folder, a folder into a folder, a file into a pane, and panes around in different panes to rearrange them. IE’s limitation of only being able to drag-and-drop text and URLs was not going to cut it. However, I realized I could use Angular to put my own layer on top and still leverage the existing browser APIs, and I’d also be able to give additional functionality. This means I could let the system realize it was not dragging around simple text, but instead an in-memory object the library could map between, say, a unique ID and an object in memory.

Step 4: Build the Solution

Building the Drag-and-Drop Directives with AngularJS

Here are the three directives I used to accomplish what I wanted to do:

  • drag-container – represents the DOM object that can be dragged around.
  • drop-container – is a DOM element that represents the area in which a drag container can then be dropped.
  • drop-area – this is very specific to my use cases, but in coming up with this configuration, I think the drop area concept is quite a new concept in that it splits a drop container into multiple virtual areas. If you saw in the example above when I was dragging files into the editor pane into the work space, if I dragged a file to the left side of an existing pane, it would offer to split that pane on the left. If I dragged it to the top, it would offer me to split it vertically on the top. I could do that on the right and the bottom as well, but not only that, I could drop it in the center, in which case it would replace the pane.

So how do you enable these sort of virtual viewports into a droppable area? That’s the concept of the drop area that I incorporated. Again, this goes back a little bit to the internals of angular, but like I said earlier, in IE you are limited to describing the data that you’re dragging as either text or as a URL. In Plunker the things that are being dropped are in fact much more complicated compound objects with a file name, with contents, with paired relationships, etc. I needed a way to map between what the browser thought I was dragging, and what the application thought I was dragging.

Thus, I created a service called $dragging, whose sole purpose was to fulfill the missing functionality I wanted.

<div drag-container="model" mime-type="text/whatever">   
  <!-- Containter that is draggable --> 
</div> 

<div drop-container accepts="['text/x-plunker-file']">   
  <div drop-target="left" on-drop="dropL(data)"></div>     
    <div drop-target="right" on-drop="dropR(data)"></div>  
    
    <!-- You can put any markup here --> 
</div>

In Angular you have this concept of scopes. A scope is a collection of data, such as in-memory objects, in-memory functions, in-memory values that are attached to directive. At the top there you have some HTML that defines a drag-container and you can see the attribute: drag-container="model". It will look in the current scope in which an element is defined and find a model value, which will represent in memory as what is being dragged if the user were to start dragging this drag-container. That information, or in-memory object, would then be considered text/whatever. This is just a hypothetical example.

<div drop-container accepts="['text/x-plunker-file']"> is basically a container defined to accept an array of different mime types. In this case, it accepts text/x-plunker files. That’s actually a mime from the new Plunker, where as long as the line type of the thing being dropped is a text/x-plunker-file, the container will accept it. Otherwise it will simply reject it and nothing will happen in the browser. In short, the drop-container defines a region that will accept any drop that passes the “accept” test. The container accepts event handlers, figures out the appropriate target, and delegates events.

You’d declare the mime type of what’s being dragged in the drag-container directive, and you do that through an attribute. The mime type is actually just a string behind the scenes, and there is no fancy mime comparison. I used mime types because they jive well with the underlying API, and the mime type is what allows me to do things like dragging files from the editor to the desktop. However, the drag container declares the type of data that’s being dragged, and it can only be one type at this point. It’s a simple string. And then the drop container (or drop targets) declare an array of strings and the examples I’ve shown there are actually formatted as mime types but that’s really up to the user. It could be FUBAR if you wanted it.

The mime type can be checked through using simple text equality. I just loop through the array of files accepted in a drop container, and if any of them matches the type that’s being dragged, then I accept.

The two narratives in the second div, <div drop-target="left" on-drop="dropL(data)"> and <div drop-target="right" on-drop="dropR(data)"> are the directives to which the drop events are delegated in the event that an item is dropped either on the left or on the right side of that drop container.

You do not have to delegate events down to drop targets if you do not have drop targets—you can have an on-drop in a drop container instead. However, if you want virtual viewports in your drop container, the system will automatically delegate events down to those points when the event is happening in your appropriate region.

Imagine this blue square is a drop container. As you are dragging within the drop container, if there is a top left corner drop target that has been declared in a DOM, that will be the element that accepts all drag and drop events. So, as you move your mouse around during a drag event, the currently active drop target will get recalculated based on where you are relative to the bounding box of that element.

Just to be clear, you do not need any drop targets, but as soon as you add them, those will be the only ones considered to establish which the closest one is. So basically I determine the active target by calculating the minimum distance between the mouse and point.

Since this drag-and-drop system is an Angular directive, I’m able to add a little bit of extra behavior here. I can add appropriate CSS style classes to active and inactive targets, so if a user is moving around, Plunker would give visual hints to let users know which regions are active, and which regions are inactive. It can give an overall indication that a drag is happening, and that a drop is acceptable in this area.

For example, you can add an -active CSS class so the drop-targets can be shown or given dashed borders.

Linking the Directives Up

If they’re not familiar with Angular directives, this might not make much sense to you. However, if you are at entry-level to Angular and are starting to do directives, this could be helpful.

.directive("dropTarget"), ["$parse"], function ($parse) {
  return { 
    	restrict: "A", 
        require: ["^dropContainer", "dropTarget"], 
        controller: "DropTargetController", 
        controllerAs: "dropTarget", 
        link: function ($scope, $element, $attrs, ctrls) { 
        	var dropContainer = ctrls[0]; 
            var dropTarget = ctrls[1]; 
            var anchor = $attrs.dropTarget || "center";
            
            dropContainer.addDropTarget(anchor, dropTarget);
         } 
    } 
}

In the fourth line, require: ["^dropContainer", "dropTarget"] says to require a drop container (which is higher up the DOM hierarchy) and a drop target (the directive itself). This line means in the link function, the final attribute controllers will be an array of two items.

Since your drop container higher up the DOM, your drop container will be ctrls[0], and your drop target will be ctrls[1]. This way, you can tell your drop container you want to register a new drop target with it.

For those who are interested in using this library, please note the drop-targets are just attachment points and are not actually designed to contain content. They will be positioned absolutely within the containing element to give them the opportunity to present visual cues to the user.

Event Handling

There’s a number of events built into the HTML5 drag-and-drop spec, and different browsers are exposed to different extents.

I’ll show you the limitations of my approach versus the others so you avoid situations where your view/DOM will get out of sync with the underlying data. My approach just lets you code in more of a single directional flow way, like all this Flux stuff that’s coming around these days.

At any rate, I’ve tried to expose these to the users of the directives:

  • on-drag-start, on-drag-end
  • on-drag-enter, on-drag-leave
  • on-drag-over
  • on-drop

You can just use these in your markup, and they will be called appropriately with injected special objects, so you’ll be able to look at the originating event as well as the data attached to the item being dragged.

Here’s a little example from a demo I’ll show you shortly:

<div drop-container            
    accepts="['text/x-dragular-piece']"            
    on-drag-enter="game.swap($index, data)">

Basically, this piece of code is a drop container that will accept a piece of a dragular board (a sliding tiles game). When you drag something into this space and it is of the right type (a dragular piece), you’re going to swap the piece that is at the current index with the piece that is being dragged. It will do this via the data object that’s exposed by Angular drag-and-drop the library.

Before I go on, let me just say that I went through a number of libraries, and you may still be wondering why I didn’t use those because all of them at some level could be hacked on. However, I felt that, at a certain level, those libraries might have been going about things backwards. The libraries give you a way to reorganize the DOM as a primary concern so you can sort things, drag them from one list to another, etc. After that has happened, they will give you the ability to react to things so to update your model.

As you can see I’ve done exactly the opposite of this approach. I give you the tools to react to the drag-and-drop events, reorganize your new model, and then it’s up to the user/application to update the view accordingly.

Demo 1: Angello

The first demo here is actually from my friend Lukas Rubbelke’s upcoming book. It’s in pre-release right now, and basically the book is about building a full Trello-like application. I helped him out by integrating this drag and drop library into it because what he wanted to be able to drag items around. In Trello you can drag cards from one status to another.

Angular & Plunker Office Hours: Demo 3

As you can see, when I drag the card it turns somewhat transparent. As a user of the HTML5 drag-and-drop spec, I didn’t have to create the logic to clone the DOM and make it semi-transparent, as it is a byproduct of using the spec. So, I can drag the card into another list, move things around, reorder things within the list and move them from one list to another. Before I even release the card, you can see the underlying card has already moved before I dropped it. Then, I’d actually sync this to the server upon drop, and that’s why the screen has into a spinner.

In this example, each column is a div, with the full height of the window as a drop container, and each little card is a drag container. This way, I don’t have to worry about different targets or virtual regions and I’m just using it at the most basic level. Thus, if you drag something into a new column, you’re going to reassign this card from one array to another (e.g. from the “to do” array to the “in progress” array). So, if you drag a card you change the underlying model, which results in the view changing. If I drag a card into the lower half of an existing card, I’m going to put it below the card, and I’m going to splice it into the underlying array after the card. If I drag it into the upper half, I’m going to splice it into the array on top of it. I can easily do a reorder-able list like this.

Demo 2: Dragular

This is the sliding puzzle demo that’s in the Github repository for this drag and drop library.

Angular & Plunker Office Hours: Demo 4

Demo 3: JSFiddle

In this demo, I’ll show you the drag-and-drop in action—let’s say I wanted to replicate the JSFiddle view.

Angular & Plunker Office Hours: Demo 5

This library, exposes a number of simple primitives and allows me to do these diverse behaviors. I can take a pane and drag it to another pane, and I can even replace an existing pane. I could try dragging a directory, but as you can see nothing happens. The app gives me a visual cue so I’d know this is not an appropriate place to drop something, and so nothing happens. The same thing happens if I try dragging a pane into the sidebar. All in all, just by leveraging a few simple primitives and a little bit of logic, I am able do this sort of complex behavior using all the same code.

Extra Demo

Let’s say I’ve come up with a cool project here and I realized that I want to share it with someone, not using Plunker, but on Dropbox or something.

In Chrome a special behavior has been enabled, and it’s exposed through Gmail, for example. You can drag items out of Gmail onto your desktop. Some smart people went in and kind of reverse-engineered how they’re doing that, so I realized that this gives us the power to do something cool very easily.

Angular & Plunker Office Hours: Demo 6

In the example above, I can take a file such as index.html and drag it straight to my desktop. I could even take an entire project and drag it to my desktop. Now, with the power of a number of different packages out there, I have actually zipped the entire project in memory in the browser and attached it to a drag event. This way, I now can drag my entire project as a zip file to my desktop.

Caveats

If you’re using this API, each time you enter a new element you need to determine on a synchronistic basis if levels being dragged into that element can be dragged into that element. You cannot fire off a server query or do any asynchronous code, you have to respond immediately. This sort of logic will have to happen in the main thread.

So, if you want to build a complex, big object and attach it to drag events, this means a little bit extra processing in the main browser thread you might normally not like. You can’t offload that to a web worker, get the result of the web worker back, and then attach it to the drag event.

Conclusion

In putting this set of directives together, I elected not to use the default scope bindings, so I’m only reading from $scope on an as-needed basis. I did this to avoid having unnecessary watchers and unnecessary levels of activity going on. That being said, there’s still quite a bit of work to optimize this a little bit more, and some future work I’d like to do is to synchronize all the events to request animation frame or at least to denounce all the handlers that do fire.

If you want to look at the code, it’s MIT-licensed and it’s on Github. You can always ping me on Twitter as @filearts or @plnkrco.

Discover and read more posts from Geoff Goodman
get started
Enjoy this post?

Leave a like and comment for Geoff

Angular/Node: Building a Command Line Tool to Generate Projects Part 2 - Angular and your FileSystem
Unit Testing with AngularJS
Application Setup: Laravel 5 & Angular 2