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.
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.
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:
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.
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.
You can see the entire code in http://jsfiddle.net/jpark7ca/g1s2wodj/40/
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/
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.
ReplyDeleteHow to extend this to have (data, event) in drop function. I want to pass also object... Thanks
ReplyDeletevery informative blog and useful article thank you for sharing with us , keep posting AngularJS5 Online Course Bangalore
ReplyDeleteIt'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...
ReplyDeleteSalesforce 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
Excellent article for the people who need information about this course.
ReplyDeleteOpennebula Online Classes
Angularjs Certification Online
Great blog.thanks for sharing such a useful information
ReplyDeleteSalesforce CRM Training in Chennai
Great blog. Thanks for sharing such a useful information. Share more.
ReplyDeletePytest Training Online
Pytest Course Online
This post is so helpfull and informative.keep updating with more information...
ReplyDeleteAngular JS Framework
AngularJS Concepts
This post is so useful and informative keep updating with more information.....
ReplyDeleteData Preprocessing Techniques In Data Mining
Preprocessing Techniques In Data Mining