Accessing $scope On The DOM Using AngularJS

Posted March 1, 2013 at 9:45 AM by Ben Nadel

Tags: Javascript / DHTML

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.


 
 
 

 
  
 
 
 

To demonstrate the scope() plugin, I thought I would try to apply the jQuery UI sortable behavior to an ngRepeat list. If you'll recall ngRepeat is an AngularJS directive that outputs a list based on a JavaScript collection. In this demo, we'll let the user manually sort that list; then, when they are done sorting, we'll rebuild the AngularJS collection based on the resulting DOM tree.

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.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="DemoController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Accessing $scope On The DOM Using AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • ul {
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • }
  •  
  • li {
  • background-color: #FAFAFA ;
  • border: 1px solid #CCCCCC ;
  • margin: 0px 0px 5px 0px ;
  • padding: 10px 10px 10px 10px ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • Accessing $scope On The DOM Using AngularJS
  • </h1>
  •  
  •  
  • <!-- Show one list of simple names. -->
  • <p>
  • <strong>Order</strong>:
  •  
  • <span ng-repeat="friend in friends">
  •  
  • {{ friend.name }}
  •  
  • <span ng-show=" ! $last ">
  • -
  • </span>
  •  
  • </span>
  • </p>
  •  
  •  
  • <!-- Show another list of SORTABLE names. -->
  • <ul bn-sortable>
  •  
  • <li ng-repeat="friend in friends">
  •  
  • {{ $index }} - {{ friend.name }}
  •  
  • </li>
  •  
  • </ul>
  •  
  •  
  • <!-- Load jQuery, jQuery UI, and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-1.9.1.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/ui/1.10.1/jquery-ui.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
  • </script>
  •  
  • <!-- Load the app module and its classes. -->
  • <script type="text/javascript">
  •  
  •  
  • // Define our AngularJS application module.
  • var demo = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the main controller for the application.
  • demo.controller(
  • "DemoController",
  • function( $scope ) {
  •  
  •  
  • // I am the collection that is being output.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Tricia"
  • },
  • {
  • id: 2,
  • name: "Sarah"
  • },
  • {
  • id: 3,
  • name: "Joanna"
  • },
  • {
  • id: 4,
  • name: "Franzi"
  • }
  • ];
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I manage the application of the sortable plugin and then
  • // update the collection to reflect the new DOM ordering. This
  • // directive ASSUMES that the sortable items are the children
  • // of the element on which the sortable behavior is being
  • // applied. It will gather the sortable expression from the
  • // actual ngRepeat directive.
  • demo.directive(
  • "bnSortable",
  • function( $parse ) {
  •  
  •  
  • // I link the element to the UI events.
  • function link( $scope, element, attributes ) {
  •  
  •  
  • // I return the ngRepeat expression. This has to
  • // be extracted from the anchor comment used in
  • // the ngRepeat transclusion.
  • function getNgRepeatExpression() {
  •  
  • var ngRepeatComment = element.contents().filter(
  • function() {
  •  
  • return(
  • ( this.nodeType === 8 ) &&
  • ( this.nodeValue.indexOf( "ngRepeat:" ) !== -1 )
  • );
  •  
  • }
  • );
  •  
  • return(
  • getNgRepeatExpressionFromComment(
  • ngRepeatComment[ 0 ]
  • )
  • );
  •  
  • }
  •  
  •  
  • // I get the ngRepeat expression from the comment
  • // that is used as the transclusion base for the
  • // ngRepeat link phase. This is in the form of:
  • // "ngRepeat: expression"
  • function getNgRepeatExpressionFromComment( comment ) {
  •  
  • var parts = comment.nodeValue.split( ":" );
  •  
  • return(
  • parts[ 1 ].replace( /^\s+|\s+$/g, "" )
  • );
  •  
  • }
  •  
  •  
  • // I get the children with the ngRepeat attribute.
  • function getNgRepeatChildren() {
  •  
  • var attributeVariants = [
  • "[ng-repeat]",
  • "[data-ng-repeat]",
  • "[x-ng-repeat]",
  • "[ng_repeat]",
  • "[ngRepeat]",
  • "[ng\\:repeat]"
  • ];
  •  
  • return(
  • element.children(
  • attributeVariants.join( "," )
  • )
  • );
  •  
  • }
  •  
  •  
  • // I get called when the user has stopped sorting
  • // AND the DOM has actually changed.
  • function handleUpdate( event, ui ) {
  •  
  • // Gather all of the children that have some
  • // variant of the ngRepeat directive.
  • var children = getNgRepeatChildren();
  •  
  • // Now, extract all of the ngRepeat items out
  • // of the dom using the scope() plugin.
  • var items = $.map(
  • children,
  • function( domItem, index ) {
  •  
  • var scope = $( domItem ).scope();
  •  
  • return( scope[ itemName ] );
  •  
  • }
  • );
  •  
  • // Now that we have the re-ordered collection,
  • // let's assign it back to the original
  • // collection so that AngularJS can update the
  • // rendering of the ngRepeat.
  • $scope.$apply(
  • function() {
  •  
  • collectionSetter( $scope, items );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // -------------------------------------- //
  •  
  •  
  • // I am the pattern of acceptable invocation for
  • // the sortable expression. It must be in the form
  • // of "REPEAT-ITEM in COLLECTION" where "collection"
  • // is something that can be get/set a value.
  • var expressionPattern = /^([^\s]+) in (.+)$/i;
  •  
  • // I am the pattern provided by the user in the
  • // nested ngRepeat directive.
  • var expression = getNgRepeatExpression();
  •  
  • if ( ! expressionPattern.test( expression ) ) {
  •  
  • throw( new Error( "Expected ITEM in COLLECTION expression." ) );
  •  
  • }
  •  
  • // Break the expression up in to parts that we can
  • // parse for item assignment. This will return an
  • // array with 3 items:
  • // [ 0 ] = Full match.
  • // [ 1 ] = First group; item.
  • // [ 2 ] = Second group; collection.
  • var expressionParts = expression.match( expressionPattern );
  •  
  • // Pluck out the names of the relevant items.
  • var itemName = expressionParts[ 1 ];
  • var collectionName = expressionParts[ 2 ];
  •  
  • // Parse the collection name so that we can easily
  • // assign to it once the sort has been updated.
  • var collectionGetter = $parse( collectionName );
  • var collectionSetter = collectionGetter.assign;
  •  
  •  
  • // -------------------------------------- //
  •  
  •  
  • // Apply the sortable items to the children.
  • element.sortable({
  • cursor: "move",
  • update: handleUpdate
  • });
  •  
  •  
  • }
  •  
  •  
  • // I configure the directive.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

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.




Reader Comments

Mar 12, 2013 at 7:17 PM // reply »
2 Comments

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.


Mar 13, 2013 at 9:41 AM // reply »
11,238 Comments

@Dave,

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:

"xxxxAZxxxx"

... and then you sorted it using a filter down to:

"AZ"

At this point, if you drag-drop-sort the list to look like this, in which Z came before A:

"ZA"

... and then removed the filter, you would have to get the following:

"xxxxZAxxxx"

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


Mar 13, 2013 at 3:55 PM // reply »
2 Comments

@Ben,

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.


Mar 23, 2013 at 9:06 AM // reply »
11,238 Comments

@Dave,

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 :)


Mar 23, 2013 at 11:01 PM // reply »
9 Comments

@Ben
may you explain this line:

  • var collectionSetter = collectionGetter.assign;

I can't find documentation for .italic

thank you


Mar 23, 2013 at 11:03 PM // reply »
9 Comments

* I can't find documentation for .assign


Mar 24, 2013 at 12:15 AM // reply »
9 Comments

sorry for disturbing,
I find the answer here http://docs.angularjs.org/api/ng.$parse


Mar 25, 2013 at 8:58 AM // reply »
11,238 Comments

@Aladdin,

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.


Apr 8, 2013 at 8:40 PM // reply »
1 Comments

@Ben,

You can see my attempt at something like this at

https://github.com/davecoates/angular-ui-sortable

Was a good learning experience, especially trying to get some test cases going with the jQuery UI stuff.


Apr 18, 2013 at 9:48 AM // reply »
11,238 Comments

@Dave,

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!


May 7, 2013 at 11:07 AM // reply »
2 Comments

Hi Ben,
About extracting the ngRepeat expression, why not simply extract it from the node attributes it in the compile phase instead of parsing the comments ?

eg:

  • var repeatExpression = tElement.find('span')[0].attributes['ng-repeat'].nodeValue,
  • collectionName = repeatExpression.match( /^([^\s]+) in (.+)$/i )[2];


May 7, 2013 at 11:15 AM // reply »
11,238 Comments

@Jujule,

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!


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools