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

Posted January 18, 2013 at 9:36 AM by Ben Nadel

Tags: Javascript / DHTML

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

Jan 18, 2013 at 1:12 PM // reply »
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.


Tim
Jan 18, 2013 at 2:17 PM // reply »
1 Comments

Hey Marc,

See:
http://www.bennadel.com/blog/328-Am-I-The-Only-One-Who-Likes-White-Space-In-My-ColdFusion-Code-.htm

:)


Jan 18, 2013 at 3:03 PM // reply »
11,314 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


Jan 20, 2013 at 10:26 PM // reply »
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


Jan 23, 2013 at 3:26 PM // reply »
64 Comments

Suicidal Plugins ... Nice.


Jan 23, 2013 at 3:30 PM // reply »
64 Comments

Regarding Reformatting Code ...

Intellij IDEA :: CTRL+ALT+L

Reformat ad-hoc ... ;-)


Jan 24, 2013 at 12:31 AM // reply »
11,314 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.


Feb 16, 2013 at 7:01 PM // reply »
2 Comments

Wonderful post. Thanks!


Feb 18, 2013 at 9:23 AM // reply »
11,314 Comments

@Dave,

Thank you, kind sir!


Apr 28, 2013 at 5:25 PM // reply »
2 Comments

Thanks for your interesting in-depth AngularJS articles !

Would be nice to make more compact code though :)


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Jun 20, 2013 at 3:15 AM
A Billion Wicked Thoughts By Ogi Ogas And Sai Gaddam
nice post i love it thanks 4 u :) ... read »
seb
Jun 20, 2013 at 2:32 AM
Working With Inherited Collections In AngularJS
@mike, @ben, The best article about scope and prototypal prototypical inheritance in angularjs is http://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical- ... read »
Jun 20, 2013 at 2:17 AM
ColdFusion NumberFormat() Exploration
Nice read thanks Ben, Is there a way to mask a negative number? Long story short in the finance sector when you go 'short' on a stock you want the price to fall this is a good thing because you are ... read »
Jun 20, 2013 at 1:09 AM
The Beauty Of The jQuery Each() Method
my html code : <html> <head> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="nss.js"> ... read »
Jun 19, 2013 at 11:31 PM
Directive Link, $observe, And $watch Functions Execute Inside An AngularJS Context
@Ben, bunch to learn indeed, but thats fun part : ) ... read »
Jun 19, 2013 at 10:41 PM
Referencing ColdFusion Query Columns In A Loop Using Both Array And Dot Notation
Burdock-roots Are you going fat day by day? You need to be good for your family and make some money too. So we bring for you a best product that helps you to be more energetic every day. You will b ... read »
Jun 19, 2013 at 9:52 PM
Working With Inherited Collections In AngularJS
I recognize the applicability of your solution, and how easy it makes to share data across multiple views or even "submodules" of rather simple application. But it seems to me that it creat ... read »
Jun 19, 2013 at 9:38 PM
Directive Link, $observe, And $watch Functions Execute Inside An AngularJS Context
@Alesei, Glad you like it. Even after working with AngularJS for months, I still get a bunch of unexpected, "$digest is already in progress". So hard to debug sometimes! ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools