Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Zac Spitzer
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Zac Spitzer

Sanity Check: $index vs. DOM In AngularJS Directives

By
Published in Comments (4)

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.

Want to use code from this post? Check out the license.

Reader Comments

16 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.

1 Comments

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel