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 2010 (Landsdown, VA) with:

Looking At Nested Event Timing And DOM Structure In AngularJS

By Ben Nadel on

In AngularJS, the DOM (Document Object Model) responds, so to speak, to changes in the view-model (VM) as defined by the Controllers. But, the Controllers don't directly control the DOM; in fact, the Controller aren't supposed to know about the DOM. This is a clean separation of responsibilities; but, it doesn't always lend itself well to a clean "mental model" of how things interact. Even after two years of AngularJS, I still find myself tripping over the timing of various events in the application. One such scenario is when nested Controllers are listening for the same event.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To see what I mean, I've put together a small demo in which three nested controllers are all listening for the same event. The only difference is that the top level controller responds to the event by destroying the nested DOM tree (which contains the other controller):

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Looking At Nested Event Timing And DOM Structure In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Looking At Nested Event Timing And DOM Structure In AngularJS
  • </h1>
  •  
  • <!-- Only show nested controllers if ngIf expression is truthy. -->
  • <div ng-if="isShowingSubtree">
  •  
  • <div ng-controller="OuterController">
  •  
  • <div ng-controller="InnerController">
  •  
  • <a ng-click="triggerEvent()">Trigger Event</a>
  •  
  • </div>
  •  
  • </div>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.19.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 ) {
  •  
  • $scope.isShowingSubtree = true;
  •  
  • // When the app controller sees the custom event, we want to hide the
  • // sub-tree that contains the other controllers.
  • $scope.$on(
  • "customEvent",
  • function( event ) {
  •  
  • console.log( "App Controller: customEvent" );
  •  
  • // Destroy the sub-tree!
  • $scope.isShowingSubtree = false;
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am a nested controller.
  • app.controller(
  • "OuterController",
  • function( $scope ) {
  •  
  • // Log out event so we can understand timing.
  • $scope.$on(
  • "customEvent",
  • function( event ) {
  •  
  • console.log( "Outer Controller: customEvent" );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the most nested controller.
  • app.controller(
  • "InnerController",
  • function( $scope, $rootScope ) {
  •  
  • // Log out event so we can understand timing.
  • $scope.$on(
  • "customEvent",
  • function( event ) {
  •  
  • console.log( "Inner Controller: customEvent" );
  •  
  • }
  • );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // Broadcast event from the top-down. Each controller is listening
  • // for this event type.
  • $scope.triggerEvent = function() {
  •  
  • $rootScope.$broadcast( "customEvent" );
  •  
  • };
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

When looking at the code, you might be tempted to believe that the nested controllers won't receive the event since the top-level controller is altering the view-model in a way that removes the nested controllers from the scope chain. However, when we run the above code and click on the "Trigger Event" link, we get the following console output:

App Controller: customEvent
Outer Controller: customEvent
Inner Controller: customEvent

As you can see, each controller received the "customEvent" event; the $broadcast() call pushed the event down through the entire scope chain.

Everything here happened exactly as it should. But that doesn't mean that it's immediately clear. The thing that you have to remember in AngularJS is that the DOM reacts to changes in the view-model by way of $watch() bindings. This means that it won't change the moment the view-model changes - rather, it will change during the next $digest in which its watch-callback will be invoked.

So, for this demo, the scope chain isn't actually altered until the "customEvent" has been pushed down through all the nested controllers. Once the event has gone through the AppController, the OuterController, and the InnerController, AngularJS then triggers a $digest which sees that the "isShowingSubtree" view-model has changed, which removes the DOM and then destroys the nested controllers.

There's not much else to say about this. It's just one of those things that can drop out of your mental model when you're busy thinking about interaction behaviors and business logic and deadlines and so on and so forth.




Reader Comments

Uhm..tricky, but I cannot imagine it differently, considering also that the order of who receives the message is not guaranteed. But is always good to keep it mind, thanks!

Reply to this Comment

@Emilio,

I think the order of the scopes is guaranteed; in that the higher-up scopes will receive it before the lower-down scopes (just a guess). But, once you are inside a scope, I think you're right - there is no guarantee as to which callback will be invoked first. I think it mostly has to do with how bound a callback first; but, I guess you can't rely on it.

Reply to this Comment

I should try, I'm not sure. But looking the documentation https://docs.angularjs.org/api/ng/type/$rootScope.Scope $broadcast should notify only to children scope. $emit instead notifies to upper scopes.
(and on $emit events can be stopped by receivers, broadcasted events no). Now I'm not sure anymore, I stopped using events from while..

Reply to this Comment

@Ben,

I am going to learn AngularJS but I am confuse between AngularJS and AngularDart. I dont know which of them is most future oriented. So, need your advice/suggestion.

Thank you

Reply to this Comment

Nice explanation, as always.

Just to note: the Javascript Demos linked project is missing a reference:
http://bennadel.github.io/JavaScript-Demos/vendor/angularjs/angular.min.js.map not found

Don
have a great day

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.