Hackviking He killed Chuck Norris, he ruled dancing so he took up a new hobby…

23Aug/160

AngularJS: Drag & Drop

Implementing drag and drop in AngularJS is pretty straight forward using directives. This also makes the code re-usable throughout your app and/or other projects as well. As always there is a few fays of accomplishing this and the number of plugins available are many. If the plugins doesn't perform what you need you can easily write your own directives to take care of this. All the examples I found out there didn't take care of updating the actual data set in the background. One even did DOM manipulation outside of AngularJS which is a big NO NO! Just taking the DOM element and removing it from one div and adding it to another doesn't really help much when manipulating data in a webapp.

Background for this example

So we have two lists that we want to drag and drop between. In this case they are hard coded but it can be something retrieved from a REST API for example. Whenever we drag an item to a new list we need to update the data set. If we then chose to send the update up to the server directly or wait for the user to hit the "save" button is a different story. So for this example I just created a basic data set like this:

var dataModel = {
  'reds': [{
    'id': 'red1',
    'text': 'This is red 1'
  }, {
    'id': 'red2',
    'text': 'This is red 2'
  }],
  'greens': [{
    'id': 'green1',
    'text': 'This is green 1'
   }, {
    'id': 'green2',
    'text': 'This is green 2'
   }]
}

We then create a controller and bring in the data simply like this:

app.controller('DragDropCtrl', function($scope) {
    $scope.data = dataModel;

That gives us the ability to use ng-repeat to display the data like this:



<div ng-app="dragDrop" ng-controller="DragDropCtrl">

<div id="reds" class="list red" droppable>

<div class="item" id="{{ item.id }}" ng-repeat="item in data.reds" draggable>
    {{ item.text }}
   </div>

  </div>


<div id="greens" class="list green" droppable>

<div class="item" id="{{ item.id }}" ng-repeat="item in data.greens" draggable>
     {{ item.text }}
    </div>

  </div>

That will be the base for our drag and drop example. As you can see in the HTML above we have the directives droppable and draggable applied to the list and the items. These directives uses native javascript functions to make the items draggable the lists able to accept drops. The draggable directive is the most straight forward, it looks like this:

app.directive('draggable', function() {
  return function(scope, element, attrs) {
    // Get the native element
    var el = element[0];
    el.draggable = true; // Make dragable
    
    // Add event listeners
    el.addEventListener(
      'dragstart',
      function(e) {
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('item_id', this.id);
        e.dataTransfer.setData('origin_id', el.parentElement.id);
        this.classList.add('dragging');
        return false;
      }, false
    );

    el.addEventListener(
      'dragend',
      function(e) {
        this.classList.remove('dragging');
        return false;
      },
      false
    );
  }
});

It just uses basic javascript functionality to make the object, in this case a fiv, draggable and attaches the ID as the data payload. It's the droppable directive that is the more interesting one and actually communicates back to the scope and in turn the controller. The droppable directive looks like this:

app.directive('droppable', function() {
  return function(scope, element, attrs) {
    // Get the native element
    var el = element[0];
    
    // Add event listeners
    el.addEventListener(
      'dragover',
      function(e) {
        e.preventDefault(); // Allow the drop
        
        // Set effects
        e.dataTransfer.dropEffect = 'move';
        this.classList.add('dragover');
        return false;
      }, false
    );

    el.addEventListener(
      'dragenter',
      function(e) {
        this.classList.add('dragover');
        return false;
      }, false
    );

    el.addEventListener(
      'dragleave',
      function(e) {
        this.classList.remove('dragover');
        return false;
      }, false
    );
    
    el.addEventListener(
      'drop',
      function(e) {
        this.classList.remove('dragover');
        
        // Get the data
        var destination = this.id;
        var item_to_move = e.dataTransfer.getData('item_id');
        var origin = e.dataTransfer.getData('origin_id');
        
        // Call the scope move function
        scope.MoveItem(origin, destination, item_to_move);

        return false;
      }, false
    );
  }
});

All this is also basic javascript for drag and drop functionality. The interesting part here is when we call the MoveItem function on the scope. The scope comes from the controller containing the list of items. So we need to define the function below on the controller scope to make this work. One thing to remember here is to be able to re-use this code throughout our application the controller scope must define this function where these directives are used.

$scope.MoveItem = function(origin, dest, item_id) {
  // Check if dropped in origin
  if (origin == dest) return;
  
  // Find item in origin array
  for (i = 0; i < $scope.data[origin].length; i++) {
    if ($scope.data[origin][i].id == item_id) {
      // Splice the item from the origin array
      var item = $scope.data[origin].splice(i, 1);
      // Push to the destination array
      $scope.data[dest].push(item[0]);
      // End loop
      break;
    }
  }
  
  // Update UI
  $scope.$apply();
}

So we basically manipulate the two arrays of items depending on the drag and drop direction. Since this was called from the directive we have to call the $apply() function on the scope so the update gets pushed to the UI. Depending on your data setup this can be handled id a service as well, the basic principal is the same.

This demo example is available on JSFiddle - AngularJS - Drag and Drop