Checkout Angular Module to implement drag-and-drop more seemlessly
In this post, I’ve created a simple demo of integration of jQueryUI draggable/droppables in AngularJS. It’s pretty straight forward to understand the code as I’ve heavily commented it so I decided not to explain it in detail.
DEMO
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html ng-app="App"> | |
<head> | |
<meta name="description" content="AngularJS + jQuery UI Drag-n-Drop" /> | |
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script> | |
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.1/angular.min.js"></script> | |
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/base/jquery-ui.css" rel="stylesheet" type="text/css" /> | |
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap.min.css" rel="stylesheet"> | |
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script> | |
<meta charset=utf-8 /> | |
<title>JS Bin</title> | |
<script> | |
// Bootstrap the Application | |
var App = angular.module('App', []); | |
// Set up a controller and define a model, list1 and list2 (empty) | |
App.controller('dndCtrl', function($scope) { | |
$scope.list1 = [ | |
{name: 'AngularJS', reject: true}, | |
{name: 'Is'}, | |
{name: 'teh'}, | |
{name: '@wesome'} | |
]; | |
$scope.list2 = []; | |
}); | |
// This makes any element draggable | |
// Usage: <div draggable>Foobar</div> | |
App.directive('draggable', function() { | |
return { | |
// A = attribute, E = Element, C = Class and M = HTML Comment | |
restrict:'A', | |
//The link function is responsible for registering DOM listeners as well as updating the DOM. | |
link: function(scope, element, attrs) { | |
element.draggable({ | |
revert:true | |
}); | |
} | |
}; | |
}); | |
// This makes any element droppable | |
// Usage: <div droppable></div> | |
App.directive('droppable', function($compile) { | |
return { | |
restrict: 'A', | |
link: function(scope,element,attrs){ | |
//This makes an element Droppable | |
element.droppable({ | |
drop:function(event,ui) { | |
var dragIndex = angular.element(ui.draggable).data('index'), | |
reject = angular.element(ui.draggable).data('reject'), | |
dragEl = angular.element(ui.draggable).parent(), | |
dropEl = angular.element(this); | |
if (dragEl.hasClass('list1') && !dropEl.hasClass('list1') && reject !== true) { | |
scope.list2.push(scope.list1[dragIndex]); | |
scope.list1.splice(dragIndex, 1); | |
} else if (dragEl.hasClass('list2') && !dropEl.hasClass('list2') && reject !== true) { | |
scope.list1.push(scope.list2[dragIndex]); | |
scope.list2.splice(dragIndex, 1); | |
} | |
scope.$apply(); | |
} | |
}); | |
} | |
}; | |
}); | |
</script> | |
</head> | |
<body ng-controller="dndCtrl" ng-cloak> | |
<div class='list1' droppable> | |
<div class='btn btn-info btn-block' ng-repeat="item in list1" data-index="{{$index}}" data-reject="{{item.reject}}" draggable>{{item.name}}</div> | |
</div> | |
<div class='list2' droppable> | |
<div class='btn btn-info btn-block' ng-repeat="item in list2" data-index="{{$index}}" data-reject="{{item.reject}}" draggable>{{item.name}}</div> | |
</div> | |
</body> | |
</html> |
Updates: More advanced version by Mita Glefler below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name='description' content='AngularJS + jQuery UI Drag-n-Drop'/> | |
<title>JS Bin</title> | |
<link href='//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/base/jquery-ui.css' rel='stylesheet'/> | |
<link href='//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap.min.css' rel='stylesheet'> | |
<style> | |
#dnd-container { | |
margin-left: auto; | |
margin-right: auto; | |
margin-top: 20px; | |
width: 568px | |
} | |
#dnd-container .ui-droppable { | |
float: left; | |
border: 2px solid fuchsia; | |
width: 250px; | |
padding: 10px; | |
height: 208px; | |
border-radius: 10px; | |
margin-top: 20px; | |
} | |
</style> | |
</head> | |
<body> | |
<div ng-app='App' id='dnd-container' ng-controller='dndCtrl' ng-cloak> | |
<div ui-drop-listener='dropListener' data-model='someArrays.list0'> | |
<div ui-draggable ng-repeat='item in someArrays.list0' data-index='{{$index}}' class='btn btn-info btn-block'> | |
{{item.name}} | |
</div> | |
</div> | |
<div ui-drop-listener='dropListener' data-model='someArrays.list1' style='margin-left: 20px'> | |
<div ui-draggable ng-repeat='item in someArrays.list1' data-index='{{$index}}' class='btn btn-info btn-block'> | |
{{item.name}} | |
</div> | |
</div> | |
<div ui-drop-listener='dropListener' data-model='someArrays.list2'> | |
<div ui-draggable ng-repeat='item in someArrays.list2' data-index='{{$index}}' class='btn btn-info btn-block'> | |
{{item.name}} | |
</div> | |
</div> | |
<div ui-drop-listener='dropListener' data-model='someArrays.list3' style='margin-left: 20px'> | |
<div ui-draggable ng-repeat='item in someArrays.list3' data-index='{{$index}}' class='btn btn-info btn-block'> | |
{{item.name}} | |
</div> | |
</div> | |
</div> | |
<script src='//ajax.googleapis.com/ajax/libs/mootools/1.4.5/mootools-yui-compressed.js'></script> | |
<script src='//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js'></script> | |
<script src='//ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js'></script> | |
<script src='//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js'></script> | |
<script> | |
(function () { | |
var app = angular.module('App', []); | |
app.controller('dndCtrl', ['$scope', 'someArrays', function ($scope, someArrays) { | |
$scope.someArrays = someArrays; | |
$scope.dropListener = function (eDraggable, eDroppable) { | |
var isDropForbidden = function (aTarget, item) { | |
if (aTarget.some(function (i) { | |
return i.name == item.name; | |
})) { | |
return {reason:'target already contains "' + item.name + '"'}; | |
} else { | |
return false; | |
} | |
}; | |
var onDropRejected = function (error) { | |
alert('Operation not permitted: ' + error.reason); | |
}; | |
var onDropComplete = function (eSrc, item, index) { | |
console.log('moved "' + item.name + ' from ' + eSrc.data('model') + '[' + index + ']' + ' to ' + eDroppable.data('model')); | |
}; | |
var eSrc = eDraggable.parent(); | |
var sSrc = eSrc.data('model'); | |
var sTarget = eDroppable.data('model'); | |
if (sSrc != sTarget) { | |
$scope.$apply(function () { | |
var index = eDraggable.data('index'); | |
var aSrc = $scope.$eval(sSrc); | |
var aTarget = $scope.$eval(sTarget); | |
var item = aSrc[index]; | |
var error = isDropForbidden(aTarget, item); | |
if (error) { | |
onDropRejected(error); | |
} else { | |
aTarget.push(item); | |
aSrc.splice(index, 1); | |
onDropComplete(eSrc, item, index); | |
} | |
}); | |
} | |
}; | |
}]); | |
app.factory('someArrays', ['$q', '$timeout', function ($q, $timeout) { | |
var deferred = $q.defer(); | |
$timeout(function () { | |
deferred.resolve({ | |
someArrays:{ | |
list0:[ | |
{name:'AngularJS'}, | |
{name:'Is'}, | |
{name:'teh'}, | |
{name:'@wesome'} | |
], | |
list1:[ | |
{name:'AngularJS'} | |
], | |
list2:[ | |
{name:'Is'}, | |
{name:'rather good'} | |
], | |
list3:[ | |
{name:'@wesome'}, | |
{name:'MooTools'} | |
]} | |
}); | |
}, 50); | |
return deferred.promise.then(function (result) { | |
return result.someArrays; | |
}); | |
}]); | |
app.directive('uiDraggable', function () { | |
return { | |
restrict:'A', | |
link:function (scope, element, attrs) { | |
element.draggable({ | |
revert:true | |
}); | |
} | |
}; | |
}); | |
app.directive('uiDropListener', function () { | |
return { | |
restrict:'A', | |
link:function (scope, eDroppable, attrs) { | |
eDroppable.droppable({ | |
drop:function (event, ui) { | |
var fnDropListener = scope.$eval(attrs.uiDropListener); | |
if (fnDropListener && angular.isFunction(fnDropListener)) { | |
var eDraggable = angular.element(ui.draggable); | |
fnDropListener(eDraggable, eDroppable, event, ui); | |
} | |
} | |
}); | |
} | |
}; | |
}); | |
})(); | |
</script> | |
</body> | |
</html> |
If you found this article useful in anyway, feel free to donate me and receive my dilettante painting as a token of appreciation for your donation.
This is cool. Do you have any suggestions on how you might test this code?
Clean code, nice work. Now it needs some css to limit the width of the items, and to give the two droppzones a border. Otherwise it’s a bit hard to see what’s happening.
Ugly colors example:
.list1{float:left;border: 2px solid fuchsia;width:250px;padding:10px;height:200px}
.list2{float:left;border: 2px solid fuchsia;margin-left:20px;background-color:#87cefa;width:250px;padding:10px;height:200px}
There’s a bug when you drag an element only a few pixels (keep it in the same container), then release. It will get pushed to the other container. Fixed here: https://gist.github.com/4409488 Also omitted the scope.$watch as it doesn’t seem necessary, and it leads to element.droppable() getting called more than once.
Hi Mita,
Thanks for the suggestions. Yep, the $watch was not needed and I also fixed the bug you raised myself. Sorry I could not merge your gist with mine but I’d attached your gist in the post for more advanced version instead. Thanks once again.
Hey there! I just would like to give you a huge thumbs up for your great information you
have right here on this post. I am returning to your web site for more soon.
this is not working
{{item.name}}
can you enable that?
Can you please share a demo to get more clarity on the issue you are talking about?