Most of the time, when you create an AngularJS directive, you know what $scope reference you are dealing with - the one that is passed into your link() function. Sometimes, however, your directive needs to deal with a collection of DOM elements, each of which has its own scope. And, as much as you don't want your DOM tree to be your "source of truth," in the right circumstances, accessing $scope references from the DOM tree can make your life a lot easier. In such cases, AngularJS provides a jQuery plugin - scope() - which allows you to access the $scope reference that is associated with the given DOM element.
Unfortunately, the context of this demonstration is a bit complicated because we have to jump through a number of hoops in order to get the proper assignment expression from the nested ngRepeat. If you want to look at a smaller example that uses the scope() plugin, take a look at my post on using jQuery event delegation in AngularJS.
That said, as you look at this demo, notice what happens when the jQuery UI Sortable plugin triggers its "update" event - we query the DOM tree to see the order of items. Then, we extract the relevant $scope value from each item and use it to rebuild the original collection.
Right now, this only works with ngRepeat elements that use the "variable in expression" repeater expression. You can also use object-based repeater expressions - ( key, value ) - but my directive is not quite that clever. Once I have extracted the ngRepeat expression, I then break it appart so that I can access the name of the "variable" as well as the name of the collection being rendered.
In order to re-assign the collection after it has been re-sorted by the user, I have to parse the collection expression using the $parse() service object (provided by AngualrJS). In doing this, I now have a Getter and a Setter method that can be used to read-from and write-to the collection, respectively.
Once the user has finished sorting the DOM elements, I then have to map the DOM tree onto a collection of items (ie. the "variable" part of the ngRepeat) and assign it back to the collection. As you can see, mapping the DOM elements onto an array is fairly straightforward because I can use the scope() plugin to access the $scope instance associated with each item in the ngRepeat.
The reassignment is done in an $apply() method so that AngularJS is made aware of the change and has a chance to re-render the HTML. Probably, a good augmentation to this demo would be to have the bnSortable attribute accept a "callback" expression to be triggered when the sort has changed.
Most of the time, you won't have to deal with the scope() plugin. Most of the time, you'll know which $scope instance you need to work with. But, in outlier cases - like this - where there's a lot of fancy DOM manipulation taking place, using scope() will make your life a lot easier.
Want to use code from this post? Check out the license.
Have you looked at angular-ui? The sortable plugin there uses ng-model to sync DOM changes. It basically uses the sortable events to track initial index and then final index and then modifies the $modelValue to re-arrange items. Works with connected sortables too.
Yeah. I've actually learned a lot about AngularJS from reading through the angular-ui source code. I actually had ui sortable implemented in an app; but, then I had to replace it with a custom build (of basically the same thing) that would take into account filtering of the list that was being sorted.
So, imagine you have a list with items:
... and then you sorted it using a filter down to:
At this point, if you drag-drop-sort the list to look like this, in which Z came before A:
... and then removed the filter, you would have to get the following:
As you can see, the sort took affect in the smallest scope possible.
This was a really funky feature to have to workout and I had to take the direction of the sort into account when figuring out where to splice and inject the moved models.
Definitely, it would have been way nicer to just use angular-ui :D
That makes sense and was something I was just thinking about (should have been sleeping)... I've been using ui-sortable but was wondering how I was going to handle sorting while filtering - now I have some ideas.
Thanks for the useful articles, they have come in quite handy as I try to get my head around how angular does stuff.
Glad to help. I'll see if I can some up with a demo of my sortable / filterable approach. It was a little bit complicated because you have to take into account which direction the sorted item is moving in (ie. left vs right) as this will influence the way in which the moved item is injected before OR after the static item. It was annoying :)
may you explain this line:
var collectionSetter = collectionGetter.assign;
I can't find documentation for .italic
* I can't find documentation for .assign
sorry for disturbing,
I find the answer here http://docs.angularjs.org/api/ng.$parse
No problem at all. If you had the question, I am sure other people did to. The documentation for $parse() is pretty small; the only thing you really have to go on is the unit test example.
You can see my attempt at something like this at
Was a good learning experience, especially trying to get some test cases going with the jQuery UI stuff.
Very cool that you took the Filtering into account. That was the biggest hurdle for me. I like that you ended up using the sortable classes to figure out where to insert - pretty clever!
About extracting the ngRepeat expression, why not simply extract it from the node attributes it in the compile phase instead of parsing the comments ?
var repeatExpression = tElement.find('span').attributes['ng-repeat'].nodeValue, collectionName = repeatExpression.match( /^([^\s]+) in (.+)$/i );
Excellent question! To be honest, I'm not that comfortable / familiar with the "compile" phase of the Directive, so I've only done some minor experiments with it. As such, I don't often think about trying to leverage it yet. But I think you're right - accessing it during that phase would have much easier than filtering for comment DOM nodes.
Thanks for the great suggestion!
What if we want to be able to select multiple elements and move them simultaneously ?
If you look at the demo at http://davecoates.github.io/angular-ui-sortable/ you can do that - just click and drag anywhere except the drag icon (the 4 pointed arrow). It requires using the selectable jquery-ui plugin in addition to sortable.
This is pretty much what I was looking for.
I am stuck implementing certain aspects of it. I don't see it in the documentation. If you have get a chance, kindly assist.
In some cases the scope() function might not work, eespecially if you create a directive with isolated scope, for this use the $('selector').isolateScope() function.
Nice work! If you had a functionality that removes something from the DOM let's say the third element (using remove() or if it was in a contenteditable div and we used backspace), do you think the code could keep the order of the internal divs and update the collection ok? Thanks
scope() is disabled in production when debug data is turned off.