Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jeanette Silvas
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jeanette Silvas ( @jeanettesilvas )

Exploring Directives, $scope, DOM Rendering, And Timing In AngularJS

By on

AngularJS provides a powerful glue that binds your View Model ($scope) to your View (HTML) using an inline, declarative syntax. As directives get linked and your $scope values change, AngularJS works quickly to update the DOM (Document Object Model) as needed. At the same time, AngularJS also needs to alert Controllers to changes in the $scope. The timing of all of this can be more than a bit tricky to understand. As such, I wanted to take a quick look at how the DOM tree structure influences which data points are available at what time.

NOTE: I didn't really get to cover everything in the video that I would have liked. I think I also said "breadth-first" a few times. I meant depth-first.

For this demo, I want to look primarily at how directives get linked; and, what those directives know about the view model and the DOM. In the following HTML document, we have a DOM tree that has three branches: Branch A, Branch B, and Branch C. Each node in each branch has a directive - bnLifecycle - which logs the linking of the directive and then watches for changes in the inherited $scope. We also have an ngRepeat directive at the bottom of Branch C which renders Span tags based on the view model.

To make this demo interesting - and to really get at the nutmeat of what's going on - we're not going to render the entire Document Object Model at load time. Rather, we're going to use ngSwitch directives to dynamically add Branch B and the ngRepeat to the DOM tree on demand.

<!doctype html>
<html ng-app="Demo" ng-controller="DemoController">
<head>
	<meta charset="utf-8" />

	<title>
		Exploring Directives, DOM Rendering, And Timing In AngularJS
	</title>

	<style type="text/css">

		div {
			border: 1px solid #CCCCCC ;
			padding: 10px 10px 10px 10px ;
		}

		div + div,
		div + section,
		section + div {
			margin-top: 10px ;
		}

	</style>
</head>
<body>

	<h1>
		Exploring Directives, DOM Rendering, And Timing In AngularJS
	</h1>

	<p>
		<a ng-click="addRepeater()">Add Repeater</a> -
		<a ng-click="addBranchB()">Add Branch B</a> -
		<a ng-click="addValue()">Add Value</a>
	</p>

	<!-- BEGIN: Nested Directives. -->
	<div id="Root" bn-lifecycle>


		<!-- Static branch. -->
		<div id="Branch-A-1" bn-lifecycle>

			<div id="Branch-A-2" bn-lifecycle>


				<span>
					Branch A
				</span>


			</div>

		</div>


		<!-- Dynamically included branch. -->
		<section ng-switch="showBranchB">


			<div ng-switch-when="true" id="Branch-B-1" bn-lifecycle>

				<div id="Branch-B-2" bn-lifecycle>

					<span>
						Branch B
					</span>

				</div>

			</div>


		</section>


		<!-- Static branch with dynamically included ngRepeat. -->
		<div id="Branch-C-1" bn-lifecycle>

			<div ng-switch="includeRepeater" id="Branch-C-2" bn-lifecycle>

				<span>
					Branch C :
				</span>

				<!-- Dynamically included ngRepeat. -->
				<span ng-switch-when="true" id="Branch-C-3" bn-lifecycle>


					<span ng-repeat="value in values" class="value">

						Value

					</span>


				</span>


			</div>

		</div>


	</div>
	<!-- END: Nested Directives. -->


	<!-- Load jQuery and AngularJS from the CDN. -->
	<script
		type="text/javascript"
		src="//code.jquery.com/jquery-1.9.0.min.js">
	</script>
	<script
		type="text/javascript"
		src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
	</script>
	<script type="text/javascript">


		// Create an application module for our demo.
		var Demo = angular.module( "Demo", [] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Define the root-level controller for the application.
		Demo.controller(
			"DemoController",
			function( $scope ) {


				// I add the dynamic branch into the DOM tree.
				$scope.addBranchB = function() {

					$scope.showBranchB = true;

				};


				// I add the ngRepeat to the DOM so that the collection
				// can be rendered.
				$scope.addRepeater = function() {

					$scope.includeRepeater = true;

				};


				// I add a value to the collection.
				$scope.addValue = function() {

					$scope.values.push( "value" );

				};


				// ---


				// I hold the collection that will be ngRepeat'd in
				// the DOM.
				$scope.values = [ "value" ];

				// I flag whether or not Branch B is in the DOM.
				$scope.showBranchB = false;

				// I flag whether or not the ngRepeat is in the DOM.
				$scope.includeRepeater = false;


				// --


				// Listen for changes in the value collection.
				$scope.$watch(
					"values.length",
					function( newValue, oldValue ) {

						// Ignore first run that results from initial
						// watching binding.
						if ( newValue === oldValue ) {

							return;

						}

						console.log( "Demo Controller" );
						console.log( "...", newValue, "in memory" );

					}
				);


			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I track the both the linking of the DOM elements and the
		// mutation of the values collection.
		Demo.directive(
			"bnLifecycle",
			function() {


				// I link the DOM element to the view model.
				function link( $scope, element, attributes ) {


					// Log that the directive has been linked.
					console.log( "Linked:", attributes.id );


					// Listen for changes in the values collection.
					// When it changes, log the in-memory length in
					// comparison to the in-DOM length of the elements
					// generated based on the collection.
					$scope.$watch(
						"values.length",
						function( newValue, oldValue ) {

							// Ignore first run that results from
							// initial watching binding.
							if ( newValue === oldValue ) {

								return;

							}

							// Gather the ngRepeat'd DOM elements.
							var values = $( "span.value" );

							console.log( attributes.id );
							console.log(
								"...",
								newValue, // In-memory.
								"in memory,",
								values.length, // In-DOM rendering.
								"in DOM"
							);

						}
					);


				}


				// Return directive configuration.
				return({
					link: link,
					restrict: "A"
				});


			}
		);


	</script>

</body>
</html>

When we first load the page, the only visible activity we have is the linking of the [static] directives. In our case, that's Branch A and half of Branch C. Our bnLifecycle directive logs its own linking on these rendered DOM nodes which leaves us with the initial console output:

Linked: Branch-A-2
Linked: Branch-A-1
Linked: Branch-C-2
Linked: Branch-C-1
Linked: Root

As you can see, directives are linked in a Depth-First, Bottom-Up approach to the DOM (Document Object Model) tree. This has some important implications about what the directives can do at link time. Since they are applied in a bottom-up approach, we know that, at link time, the descendant nodes of the directive's element have all been fully rendered. We also know that the previous-sibling nodes of the directive's element have been fully rendered as well. What we don't know about the DOM, however, is anything about the parent node or the next-sibling nodes of the directive's element.

NOTE: The ngController directive is a special kind of directive that has its own link timing. Controllers are linked in a top-down manner, which will become evident when we look at the $watch()-based output.

Now that we have our initial DOM rendering, I'm going to click on the following links in order:

  1. Add Repeater
  2. Add Branch B

This will dynamically add the rest of Branch C (the repeater) and the entirety of Branch B to the current DOM tree. As these elements are added to the document, AngularJS links the bnLifecycle and ngRepeat directives, giving us some more output in our console:

Linked: Branch-C-3
Linked: Branch-B-2
Linked: Branch-B-1

Again, we see that AngularJS links directives in a depth-first, bottom-up approach.

Before we perform the next action, let's be very cognizant about the order of operations in which the DOM has been constructed:

The ngRepeat was added to the DOM before Branch B was added to the DOM. In other words, Branch B was added to the DOM after AngularJS has already linked the ngRepeat directive.

Let that sink in for a second as it's critical to understanding the timing of all of this.

Ok, now let's use the "Add Value" link to add a value to our rendered collection. And remember, each directive (and the root Controller) is watching the $scope for changes in the length of the collection. And, when this change occurs, the new value is logged along with the directive's view into the currently-rendered Document Object Model.

When I click on the "Add Value" link, a new item is added to the "values" collection which triggers all of the relevant $watch() bindings. This results in the following console output:

Demo Controller
... 2 in memory
Branch-A-2
... 2 in memory, 1 in DOM
Branch-A-1
... 2 in memory, 1 in DOM
Branch-C-2
... 2 in memory, 1 in DOM
Branch-C-1
... 2 in memory, 1 in DOM
Root
... 2 in memory, 1 in DOM
Branch-C-3
... 2 in memory, 2 in DOM
Branch-B-2
... 2 in memory, 2 in DOM
Branch-B-1
... 2 in memory, 2 in DOM

At first, this output might not make any sense at all. On the one hand, things sort of look like they are firing in a depth-first, bottom-up approach; but, on the other hand, this clearly doesn't hold true for all of the log values. The fact is, $watch() bindings have almost nothing to do with the struct of the DOM at all. $watch() bindings are triggered in the same order in which they were bound; the DOM merely influences the order in which directives are linked, which in turn, influences the order in which the $watch() bindings are created.

Oh, and don't forget, ngRepeat is just a directive - like bnLifecycle. And, just like bnLifecycle, it uses $watch() bindings to know when the values collection has changed. This means that the ability of ngRepeat to update the DOM is subservient to the relative timing of its own $watch() bindings. This is why Branch B knows about the DOM changes precipitated by Branch C-3 - the $watch() bindings in Branch B were created after Branch C-3 was added to the DOM.

The timing of all of this stuff can be a bit complicated. Hopefully this exploration has helped clear things up a bit. Since nested directives can't always depend on a predictable structure, you can use "directive controllers" to facilitate inter-directive communication; but that's something I'm still trying to wrap my head around.

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

Reader Comments

6 Comments

Style question Ben: what's with all the empty lines? I distrust code the more I have to scroll to understand it. One Line should equal = One Logical Statement wherever possible.

15,674 Comments

@Marc,

When I see too many lines of code in a row, I start to panic :) Having the white space is just a personal preference - it helps me think about each chunk of code a bit more independently.

You're not the only one who dislikes my style - Simon Free (with the support of many fans) actually create a IDE plugin that would strip out my excessive whitespace:

http://debenification.riaforge.org/

... get it, De-Ben-ification :D

7 Comments

Loving your posts on AngularJS Ben. These videos/blog posts are great!

I have a favor to ask though... can you please please please use a friendlier video component - html5, so i can watch without flash :)

Thanks!
Steve

15,674 Comments

@Steve,

Yeah, I really gotta figure out the video stuff. I just JING to record the videos; and I think if I upgrade to their pro account, they can do HTML5 videos. I'll look into that ASAP.

2 Comments

this is supposedly an antipattern:
// Gather the ngRepeat'd DOM elements.
var values = $( "span.value" );

do you have a reason for intentionally using it despite the convention? (wrapping dom tags in $( - supposedly this is done by default/wondering if 'double' wrapping it is of any advantage(???)

Tia,

15,674 Comments

@Greg,

I don't think there is any double-wrapping. The "Span.value", in this case, is not the element associated with the directive - in which case, it *would* be double-wrapping. What I'm doing is actually locating the span.value matches lower-down in the DOM tree. Basically, I'm just querying the DOM via old-school jQuery.

That said, I have many times done this:

function link( $scope, element, attribute ) {
	var target = $( element );
}

... that IS double-wrapping, and if there's code like that it's because I didn't realize that the element passed into the directive was already a jQuery/jQLite element. :)

2 Comments

@Ben Nadel
ironically, just read about it today - i don't know why, but the example code here doesn't get evaluated in the later jquery or angularjs (jquery 2.x.x && angularjs 1.2.x)

is there a paradigm shift at a conventional level? something seems to be different enough for it to not be recognizing the setup.
so far, been scoping out the ng___ commands - nothing looks different enough to break it so far.

would appreciate a clue if you know though.
thanks again,
<g>

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