Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jared Rypka-Hauer
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jared Rypka-Hauer

Looking At Nested Event Timing And DOM Structure In AngularJS

By
Published in Comments (5)

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.

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

Reader Comments

2 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!

15,811 Comments

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

2 Comments

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

6 Comments

@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

2 Comments

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

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