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 CFUNITED 2008 (Washington, D.C.) with:

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

By Ben Nadel 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.




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

Reply to this Comment

@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

Reply to this Comment

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

Reply to this Comment

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

Reply to this Comment

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,

Reply to this Comment

@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. :)

Reply to this Comment

@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>

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.