Drag and Drop with AngularJS and jQuery UI

I actually haven’t used jQuery UI much, and been just tooling around with AngularJS.  That’s why when a recent project came up at work which had the typical ASAP schedule, I thought: “Let’s put AngularJS to work!”  Also, the application I was building was an internal tool, so it could be a little rough around the edges and customers/clients wouldn’t make fun of me if something wasn’t quite right.

So….low risk, let’s dive right into Angular!  I also had the assumption that if Angular was all I thought it was cracked up to be, it would save me lots of time, and allow me and others very nice visibility to how my project worked so they could make updates very simply, right in the HTML.

Boy was I right.  I felt like Angular sped things up and gave me what I wanted, with a nice easy, predictable structure.

What I didn’t expect was that I’d be mixing jQuery UI in.  What I REALLY didn’t expect was how SLICK Angular makes it to mix things like jQuery UI in.

 

The Problem: I want to drag something to a drop zone

Pretty simple right?  Of course, of course I can write this from scratch in Javascript.  And if I wrote it from scratch, I’d probably cut corners and…..not necessarily make it all fancy with tags and attributes for my Angular setup.  No, I’d probably loop through all the elements in my list that I’d like to drag, attach onmouseout, onmouseover, and onmousemove listeners.  And then keep a separate list of all my drop zones and check positions of things while they are being dragged.

PAIN IN THE BUTT.  I don’t mind doing cool things by hand, but things that are readily available elsewhere? Boring. So I turn to Google and type in “AngularJS drag and drop”.  I came across this gist:

https://gist.github.com/codef0rmer/3975207

Given what AngularJS is supposed to do, I didn’t expect to find any functionality in Angular to support drag and drop.  Indeed I didn’t, but what this gist shows is the ability to declare your own attributes in HTML and have Angular process them, adding your own logic to what those things do.  THAT IS SLICK!

 

Angular Directives

So let’s take “draggable” and “droppable” as our primary examples for an Angular directive.

In our HTML, create a div with your own custom attribute on it:

<html ng-app=”App”>

<div class=”something” draggable></div>

<div class=”somethingelse” droppable></div>

</html>

Obviously these two attributes mean nothing to HTML, so we want to process them with Angular.  Luckily we’ve declared our Angular App as “App”.  We can then process our directives:

var App = angular.module('App', []);
App.directive('draggable', function() {
    return {
        // A = attribute, E = Element, C = Class and M = HTML Comment
        restrict:'A',
        link: function(scope, element, attrs) {
        }
}

So, above, we’ve made a reference out of our “App” module (our HTML page/app).  Then we’ll add a directive – which is simply saying, anything labeled “draggable” – return an object that does something.

I haven’t tried anything except “restrict:’A'” yet, however this is pretty fancy.  We filter this behavior by attributes, elements, classes, or comments.  For our example, we’re restricting by our attribute.

Then the link process.  Here you define what you’d like to when Angular finds the attribute.  We’d like to hook up the jQuery UI drag behavior:

      element.draggable({
        revert:true
      });

Right! So all we do here, is take the element that the Angular directive gives us and call the jQuery UI method “draggable” on it.  I left the gist author’s revert:true in there, cause it’s pretty nice.  If the drag isn’t accepted, its animates back over to where it started.

Similar deal with droppable:

App.directive('droppable', function($compile) {
    return {
        restrict: 'A',
        link: function(scope,element,attrs){
            element.droppable({
                accept: ".itemHeader",
                hoverClass: "drop-hover",
                drop:function(event,ui) {}
    }
}

We process the directive, filter by attribute, and on link call the “droppable” jQuery UI method.

jQuery UI has some nice options here for droppable.  It can be set to only accept items of a certain class or ID (comma delimited lists inside the quotes are cool here).  You can specify a hoverClass as well.  In my case,  the “drop-hover” CSS adds a white border to the element.  When you drag your draggable over a dropzone, then the dropzone will light up with a white border.

And then the drop functionality.  What happens when you drop it?  I have my own logic, you’ll certainly have your own.  An important note is how to get the drag or drop elements using these parameters that you get:

dragged = angular.element(ui.draggable).parent();
dropped = angular.element(this);

This, of course is in the “droppable” method. If you were in the other “draggable” method, angular.element(this) would be the draggable.

Wait theres more! I had to do a little trickery in my application. See, it turned out that not everything could be dragged to everything else. If you picked up a draggable, you can only drop it’s related dropzones. Other draggables have other dropzones.

I needed a good way to NOT let the user drag something where they shouldn’t depending on what they picked up, but also make them SEE what they can or can’t drop on!

 

Start/Stop Drag Listeners

Luckily, JQuery UI gives us a few more draggable options:

App.directive('draggable', function() {
    return {
        // A = attribute, E = Element, C = Class and M = HTML Comment
        restrict:'A',
        link: function(scope, element, attrs) {
            element.draggable({
                revert:true,
                start: function(event, ui) {}
                stop: function(event, ui) {}

So, we can have something happen when the user starts to drag something AND when they stop.  So for me, when “start” is fired, I loop through my list of dropzones.  If it’s determined that the drag is not allowed on a given dropzone, I’ll add a “reject” class to the dropzone.  In my CSS, I fade the background and the text and everything so it looks dim.  And then in my element.droppable.drop function, I check to see if the “reject” class is present – and don’t do anything if so.

In my stop function here, I simple go through and remove all the reject classes from everything, since the user has stopped dragging.

 

Lots of options here for doing drag and drop!  And a pretty nice way of Angular opening the door to other tools as we add non-Angular functionality.

9 thoughts on “Drag and Drop with AngularJS and jQuery UI”

  1. Hey Benjamin, nice write up on this. The way you explained your flow here was extremely easy to comprehend. I’ve been eying up Angular for some time, but my JavaScript is intermediate at best. Thanks for making things a bit clearer.

    Two things. Where do you include those dragged/dropped parameters? And in the start/stop functions, you add your own JS in the function to tell if the object has been given a “reject” class, correct? Just want to know what code is yours, and what is provided by Angular and jQuery UI.

    1. Yes, the reject class is my own. And to be honest – it’s might be a little above and beyond what most people need. jQuery UI already gives you the “accept” parameter. You can restrict what gets accepted based on that alone – with no need for my custom “reject” implementation.

      However, in my case I not only needed to “accept” certain elements, but add logic on top of that which needed to be a little more granular than the “accept” option could offer.

      in my draggable method, I added:

      start:function(event,ui) {
        if (reject == true) { // I omitted the code to actually set the reject boolean to what works for my app
          $(myelement).addClass("reject");
        } else {
          $(myelement).addClass("accept");
        }
      

      then in my droppable method, I added:

      drop:function(event,ui) {
        if ($(this).hasClass("reject")) {
          return;
        }
        ....
      

      In that fashion, my method doesn’t continue on to do the drop stuff I want done if it’s “rejected”.

      Now that I’m spelling this all out for you, I do realize that there may be a way to simplify and just use the “accept” option. I’d have to mull it over – but for educational purposes, even if this isn’t the most straightforward way, it’s good to see what’s possible!

  2. Thanks for this write, I’m using it but… I’m having an issue with the drag. It’s slow kind of choppy. Then I added the opacity clone and the position is all messed up have you experienced this?

    1. I hadn’t experienced the stuff you are saying. Do you have any scripts running as you drag? Also perhaps your draggable item has a lot more stuff (many many divs, spans, other DOM elements) than I have. I have a few spans inside a div element – and it runs quite smooth.

      I had another project (not this Angular Drag and Drop project) where I was running jQuery.find on a timeout that occured every 50ms. It was a stupid move on my part since it takes time to run and made my movement choppy as well. If you do anything like that, it can be best to cache what element you find in a var so you don’t have to keep looking it up.

      But of course…you don’t indicate that you are doing anything like that, so I’m not sure.

      On the bad positioning, mine works well – though I would imagine that behind the scenes the element that is being dragged uses fixed/absolute positioning for the style. Perhaps you are overriding this in your style sheets somewhere for the draggable/cloned element?

      Good luck!

  3. It works nicely with Chrome, Firefox and IE8+, but it doesn’t work at all in IE7. I’ve followed the instructions on Angular’s website to get AngularJS work with IE7 and the app works, but the dragging doesn’t work with the IE7 Browser/Document Modes. Is it something I should have added to the code or does it simply not work at this time and are there plans to make it work?

    I know IE7 is old, but it’s hard to get clients update their browsers when their bosses prevent them from doing so…

    1. Good comment – I typically do my dev on a Linux machine so hadn’t tried it in IE7. I do wonder if jQuery UI or Angular is the showstopper for that one. From some light Googling, perhaps its just the jQuery UI draggable option that doesn’t work:

      http://stackoverflow.com/questions/6815260/jquery-draggable-problem-in-ie7

      I can find a few various comments similar to this with workarounds

      I’m a little busy at a conference now to dig into what’s wrong, but if you figure it out please do update!

  4. Thanks for the write up. it was definitely very helpful. I am struggling with one thing – i have multiple dropzones on my page and sometimes they are nested as well, the user should be able to drop the drag element in any of the element – either child or parent. the problem is i want to drop the element only once in the child element as of now what happens is that on drop the element gets dropped in the child & parent both. how can i loop through the dropzones and identify the exact child element where to drop instead of duplicating the drops. would appreciate any ideas you may provide.

    1. Definitely many ways to do this I’m sure. In directives, you can gain access to the containing controller. You might want to talk to the controller when the droppable area is initialized to report back and create a central drop area list.

      When you drop the element, run the action up to the controller where you have central logic whether to accept the drop or not. You may even be able to have a directive inside a directive in this regard (where the outer directive has some logic to control all of this). When you centralize your logic like this – you can keep a priority order of which dropzones you check first, and you should be able to order it to use the children first rather than the parent (or vice versa).

  5. thanks for the prompt response. im relatively new to angular world and somehow im unable to call the controller method from the directive, though its very much there.
    here is the code.. what do you think im doing wrong ?

    ‘use strict’;
    rmApp.controller(‘DragDropController’,
    function DragDropController($scope){
    $scope.droppables = [];

    $scope.sayHello = function(){
    console.log(‘hello’);
    }
    });

    rmApp.directive(‘rmDroppable’, function($compile) {
    return {
    restrict: ‘A’,
    controller:’DragDropController’,
    link: function(scope,element,attrs,controller){
    scope.sayHello();

    }
    }
    });

Leave a Reply