AngularJS is an amazing framework. Together with jQuery and jQuery UI is a killer combo. But sometimes it’s really difficult to make them work together.
Task
Imagine we have a box (div) and inside some elements that we can drag around.
1 2 3 4 5 6 |
<div id="items" > <span>drag me 1</span> <span>drag me 2</span> <span>drag me 3</span> <span>drag me ...</span> </div> |
Solution #1
We will attach the jQuery UI draggable inside of the directive that we added to html (items-drag)
1 2 3 4 5 6 |
<div id="items" items-drag> <span>drag me 1</span> <span>drag me 2</span> <span>drag me 3</span> <span>drag me ...</span> </div> |
and create an app with a directive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var app = angular.module('app', []); app.directive('itemsDrag', function() { return { link: function(scope, element, attrs) { // element == $('#items') element.find('span').draggable(); scope.$on('$destroy', function() { element.find('span').off('**'); }); } }; }); |
This works, but it’s totally unrealistic. In real life we probably load items from somewhere and populate the div. So let’s try that.
Solution #2
We add the controller ItemsController to the HTML with ng-repeat
1 2 3 4 5 |
<div ng-controller="ItemsController"> <div id="items" items-drag> <span ng-repeat="item in items">drag me {{ item }}</span> </div> </div> |
and add controller to our app.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
var app = angular.module('app', []); app.controller('ItemsController', function($scope, $timeout) { $scope.items = []; // we will simulate loading items from the server with timeout $timeout(function() { $scope.items = [1, 2, 3, 4, 5, 6]; }, 4000); }); app.directive('itemsDrag', function() { return { link: function(scope, element, attrs) { element.find('span').draggable(); scope.$on('$destroy', function() { element.find('span').off('**'); }); } }; }); |
This will NOT work. Because when directive is loaded, it will find all spans and attach draggable to them. But because items are empty, it won’t find any spans. When they are loaded from the server ($timeout executes), ng-repeat will repeat and show items, but draggable will not be attached.
We can solve this by adding $watch and watching when items update and attach draggable. Let’s just update our directive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
app.directive('itemsDrag', function() { return { link: function(scope, element, attrs) { scope.$watch('items', function(items) { // items changed, let's reattach again element.find('span').off('**').draggable(); scope.$on('$destroy', function() { element.find('span').off('**'); }); }); } }; }); |
This works. Great. But actually there is a big problem. When ng-repeat is adding the elements into the DOM, $watch method is fired and draggable is attached to items. Problem is that this happens during ng-repeat so draggable is not attached to all elements. What now?
Solution #3
We need to somehow wait for ng-repeat to finish and that all elements/items are loaded into DOM. Based on my research, there is no bulletproof way. Some suggest to use timeout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
app.directive('itemsDrag', function() { return { link: function(scope, element, attrs) { scope.$watch('items', function(items) { // attach draggable when all elements are loaded into DOM // (hopefully) $timeout(function() { element.find('span').off('**').draggable(); }, 500); scope.$on('$destroy', function() { element.find('span').off('**'); }); }); } }; }); |
This solution has one big problem. We cannot never set the right timeout time. If we set too small, it won’t work if we have a long list of items. If we set too large, then we can impact the user experience.
Solution #4 – The working one
The working solution is actually really simple and works for small or large lists of items without impacting the user experience.
1 2 3 4 5 |
<div ng-controller="ItemsController"> <div id="items"> <span ng-repeat="item in items" items-drag>drag me {{ item }}</span> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
var app = angular.module('app', []); app.controller('ItemsController', function($scope, $timeout) { $scope.items = []; // we will simulate loading items from the server with timeout $timeout(function() { $scope.items = [1, 2, 3, 4, 5, 6] }, 4000); }); app.directive('itemsDrag', function() { return { link: function(scope, element, attrs) { element.draggable(); scope.$on('$destroy', function() { element.off('**'); }); } }; }); |
We updated the directive’s element. We don’t attach directive to div#items anymore, but to each span. When each element/item is added to DOM, directive is fired and attaches draggable. So there is no timeouts or watching if $scope.items changed.
For me, this is the best way to combine AngularJS with jQuery UI – Draggable. Of course it also works for any plugin.