Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with:

Sanity Check: $index vs. DOM In AngularJS Directives

By Ben Nadel on

With AngularJS, there is such a strong separation between the DOM (Document Object Model) and your Controller / Model / Service components, that I sometimes find myself unsure as to when I can depend on the existence of dynamically-generated DOM nodes within AngularJS directives. The other day, I had a moment of anxiety about the $index value added by the ngRepeat directive; and, I found myself having to run a sanity check on how the DOM lines up with the $index values.


 
 
 

 
  
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

In an ngRepeat loop, AngularJS automatically adds an $index value (among others) to the item-based $scope that gets created in the loop. In this sanity check, I wanted to see if watching for changes in the $index value would accurately reflect changes in the current DOM structure. In this case, I'm logging the number of generated LI elements whenever the $index changes.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Sanity Check: $index vs. DOM In AngularJS Directives
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Sanity Check: $index vs. DOM In AngularJS Directives
  • </h1>
  •  
  • <form ng-submit="addFriend()">
  •  
  • <input type="text" ng-model="form.name" />
  • <input type="submit" value="Add Friend" />
  •  
  • </form>
  •  
  • <ul>
  • <li
  • ng-repeat="friend in friends"
  • class="friend">
  •  
  • <!--
  • As each ngRepeat item is rendered, we're going to
  • listen for changes on the auto-generated $index value.
  • -->
  • <span bn-index-watch>{{ friend.name }}</span>
  •  
  • ( <a ng-click="removeFriend( friend )">Remove</a> )
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I am the collection of friends being rendered.
  • $scope.friends = [
  • {
  • name: "Sarah"
  • }
  • ];
  •  
  • // I am the form-input collection for ngModel.
  • $scope.form = {
  • name: ""
  • };
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I add a new friend, using the form input.
  • $scope.addFriend = function() {
  •  
  • $scope.friends.push({
  • name: $scope.form.name
  • });
  •  
  • $scope.form.name = "";
  •  
  • };
  •  
  •  
  • // I remove the given friend from the collection.
  • $scope.removeFriend = function( friend ) {
  •  
  • $scope.friends.splice(
  • $scope.friends.indexOf( friend ),
  • 1
  • );
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I watch for changes in the $index and compare it to the DOM.
  • app.directive(
  • "bnIndexWatch",
  • function() {
  •  
  • // I wire the $scope to the DOM.
  • function link( $scope, element, attributes ) {
  •  
  • $scope.$watch(
  • "$index",
  • function( newValue, oldValue ) {
  •  
  • // Collection the rendered DOM elments and
  • // the index in the context of the current
  • // directive / DOM element.
  • // --
  • // NOTE: We are making the index 1-based
  • // for an easier-to-read comparison.
  • var friendCount = $( "li.friend" ).length;
  • var indexCount = ( newValue + 1 );
  •  
  • console.log( indexCount, ",", friendCount );
  •  
  • }
  • );
  •  
  • }
  •  
  • // Return directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

After adding three friends to the collection, I get the following console log output:

1 , 1
2 , 2
3 , 3
4 , 4

As I added items, you can see that the changes in the $index value match the changes in the DOM.

After removing three friends from the top of the list (in order to actually force $index changes on existing items), I get the following console log output:

1 , 3
2 , 3
3 , 3
1 , 2
2 , 2
1 , 1

As I deleted items, you can see that the changes in the $index value match the changes in the DOM.

Sanity check accomplished! The $index value is an accurate way to gauge the state of the Document Object Model (DOM) in the context of an AngularJS ngRepeat directive.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

I had NOOOO idea you could pass the entire friend object into a method like this: removeFriend( friend ) .

I was always using some ID of the object and passing that around back and forth between views/controllers/services.

Checking this out tonight.

Neat and thanks.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.