AngularJS custom directive to handle drag and drop (HTML 5)

Recently, I had a chance to develop a single page application that uses "Drag and Drop" feature of HTML 5. This application allows users to move items (div or anchor tag) from one container to another. Since I had to implement this using AngularJS, I thought it could be fun to create a custom directive for handling drag and drop events. Although this is a very simple directive, I thought it could be useful to share my code for those who need to implement a similar feature in AngularJS.

If you want to see this feature in action, click this jsfiddle link: http://jsfiddle.net/jpark7ca/g1s2wodj/40/

Before diving into AngularJS code, I think it will be useful to know how HTML 5 "drag and drop" feature (standard) works in modern browsers. By the way, if you use IE9, you need to use anchor tag as draggable item. In order to make a tag draggable, you need to add an attribute called "draggable" and set it to true, and then, specify what should happen when the tag starts to be dragged as shown below. The ondragstart attribute calls drag(event) function.


<div id="1" draggable="true" ondragstart="drag(event)" >John Doe</div>


Typically, the dataTransfer.setData() function will be called in drag(event) function to store the value of the dragged tag (e.g., id value).

function drag(evt){
   evt.dataTransfer.setData("text", evt.target.id);
}

Finally, I have to add a drop area (e.g., div) where I can drop dragged tags. The ondragover attribute calls a function, dragover(event) to allow tags to be dropped. In order to allow a drop, I have to prevent the default handling of the element. In addition, the ondrop attribute calls a function, drop(event) which handles actions such as get the id value of the dragged item and append it to the drop area.


<div ondragover="dragover(event)" ondrop="drop(event)">DROP AREA<div>

function dragover(evt) {
   evt.preventDefault();
}

function drop(evt) {
   evt.preventDefault();

   var id = evt.dataTransfer.getData("text");
   evt.target.appendChild(document.getElementById(id));

}

My motivation to create a custom directive was due to the fact that AngularJS simply does not provide directives (ng-draggable, etc.) to handle the HTML 5 drag and drop feature. Fortunately, AngularJS provides a mechanism to create custom directives to expand current AngularJS implementation when certain features are missing. So, I decide to create jp- directive (custom) to address this.

For example, if you use my custom directive, your code should look like this:


<div ng-app="testApp">
    <div ng-controller="TestAppController as app">
        <!-- drop area -->
        <span>Drop here: </span>
        <div jp-draggable jp-drop="app.OnDrop(event)" jp-dragover="app.OnDragOver(event)" class="drop">            
        </div>
        <!-- list -->
        <div>
            <ul class="list-group">
                <li class="list-group-item" ng-repeat="p in app.list">
                    <div id={{p.id}} jp-draggable jp-isdraggable="true" jp-dragstart="app.OnDragStart(event)" >
                        <i class="fa fa-arrows"></i> {{p.title}}
                    </div>
                </li>
            </ul>
        </div>
    </div>
</div>


By the way, this post is not intended to teach AngularJS directives. As you can see above, jp-draggable directive uses scope attributes to access parent scope data, in this case, TestAppController. For example, jp-dragstart attribute calls TestAppController's function called OnDragStart(event). In other words, I am creating a custom directive with isolated scope so that any parent scope data can only be accessible via callbacks (e.g., OnDragStart) in parent scope.

When jp-draggable is specified in HTML tag, the link function will be executed. First, it will add "draggable" attribute with true value and then, register parent callbacks with corresponding javascript events such as dragstart, dragover and drop. I won't go into details because the code below is self-explanatory.


.directive('jpDraggable', function() {
 return{
  scope: {
   myDragStart: '&jpDragstart',
   myDrop: '&jpDrop',
   myDragOver: '&jpDragover',
   myDragLeave: '&jpDragleave',
   myDragEnter: '&jpDragenter',
   myIsDraggable: '=jpIsdraggable'
  },
  link: function(scope, element, attrs)
  {   
   if(scope.myIsDraggable == true){
    //Add draggable attribute and set it to true
    if(!element.attr('draggable')){
     element.attr('draggable', true);
    }
   }
      
   element.on('dragstart', function(evt){
    scope.myDragStart({event: evt});
   });
   
   element.on('drop', function(evt){    
    scope.myDrop({event: evt});
   });
   
   element.on('dragover', function(evt){
    scope.myDragOver({event: evt});
   });
   
   element.on('dragleave', function(evt){
    scope.myDragLeave({event: evt});
   });
   
   element.on('dragenter', function(evt){
    scope.myDragEnter({event: evt});
   });
  }
 };
});


Finally, I have to implement callback functions in parent scope, in this case, TestAppController. Instead of using dataTransfer.setData() to pass the id value, the dataid (in controller scope) variable will be used to set and get the dropped element's id. However, if you use Firefox, you still need to add evt.dataTransfer.setData("text", evt.target.id) in OnDragStart because Firefox won't allow you to drag the element without setting the value of the dragged data via dataTransfer.setData method.


    app.dataid = 0;
    
 app.OnDragStart = function(evt) { 
  app.dataid = evt.target.id;
 };
    
    app.OnDragOver = function(evt) {
  evt.preventDefault();   
 };
    
    app.OnDrop = function(evt) {    
  evt.preventDefault();
        evt.target.appendChild(document.getElementById(app.dataid));
 };    

You can see the entire code in http://jsfiddle.net/jpark7ca/g1s2wodj/40/

Comments

  1. All the concepts I have learnt at online Angularjs training, are greately discussed on this blog indepth. Thanks for your research and time for maintaining this website.

    ReplyDelete
  2. How to extend this to have (data, event) in drop function. I want to pass also object... Thanks

    ReplyDelete
  3. very informative blog and useful article thank you for sharing with us , keep posting AngularJS5 Online Course Bangalore

    ReplyDelete
  4. It's interesting that many of the bloggers to helped clarify a few things for me as well as giving.Most of ideas can be nice content.The people to give them a good shake to get your point and across the command...
    Salesforce Training in Chennai

    Salesforce Online Training in Chennai

    Salesforce Training in Bangalore

    Salesforce Training in Hyderabad

    Salesforce training in ameerpet

    Salesforce Training in Pune

    Salesforce Online Training

    Salesforce Training

    ReplyDelete
  5. Excellent article for the people who need information about this course.

    Opennebula Online Classes
    Angularjs Certification Online

    ReplyDelete
  6. Great blog. Thanks for sharing such a useful information. Share more.
    Pytest Training Online
    Pytest Course Online

    ReplyDelete
  7. This post is so helpfull and informative.keep updating with more information...
    Angular JS Framework
    AngularJS Concepts

    ReplyDelete

Post a Comment

Popular Posts