Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

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.

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.