Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Joel Hill and Matt Vickers and Shawn Grigson and Jonathan Rowny and Jonathan Dowdle and Christian Ready and Oscar Arevalo and Jeff McDowell and Steve 'Cutter' Blades
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Joel Hill@Jiggidyuo ) , Matt Vickers@envex ) , Shawn Grigson@shawngrig ) , Jonathan Rowny@jrowny ) , Jonathan Dowdle@jdowdle ) , Christian Ready@christianready ) , Oscar Arevalo@oarevalo ) , Jeff McDowell@jeff_s_mcdowell ) , and Steve 'Cutter' Blades@cutterbl )

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.




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

I got here for the same reason as @John Allen, i.e. - get the index for deleting a an entry from an array ViewModel.

Thanks for the showing the easy way to do that Ben.

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.