Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Luke Brookhart
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Luke Brookhart@lukeb )

How Scope.$broadcast() Interacts With Isolate Scopes In AngularJS

By Ben Nadel on

After looking at how to unbind scope-event handlers in AngularJS yesterday, I got curious as to how events interact with isolate scopes. I'm still relatively new to isolate scopes, so my mental model isn't super strong. I know they don't "prototypically" inherit data from their parent; but, they're still part of the Scope hierarchy. So, what does this mean for the Scope-based event system?


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To explore this scenario, I set up an AppController, an Isolate Scope directive, and an InnerController that - from an HTML standpoint - are all nested. However, from a Scope standpoint, we know that this is not quite so simple. When the application is bootstrapped, I start broadcasting events from the AppController. The Isolate scope then listens for this event and also attempts to watch for changes in the related scope property.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • How Scope.$broadcast() Interacts With Isolate Scopes In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • How Scope.$broadcast() Interacts With Isolate Scopes In AngularJS
  • </h1>
  •  
  • <div bn-isolate>
  •  
  • <p ng-controller="InnerController">
  • Look at the console output.
  • </p>
  •  
  • </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.26.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, $interval, $rootScope ) {
  •  
  • // I keep track of the number of times the ping event is being broadcast
  • // down from the root of the application.
  • $scope.pingCount = 0;
  •  
  • // Continuously broadcast an event down the scope tree. Note that the
  • // PingCount is being incremented as part of the broadcast.
  • $interval(
  • function handleInterval() {
  •  
  • $scope.$broadcast( "ping", ++$scope.pingCount );
  •  
  • },
  • 1000
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I create an isolate scope directive to investigate how events interact with
  • // both parent scope and "inner" scope.
  • app.directive(
  • "bnIsolate",
  • function() {
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • // I watch for changes to any prototypically inherited pingCount
  • // properties. Since this is an isolate scope, this should be
  • // Undefined in the local scope.
  • scope.$watch(
  • "pingCount",
  • function handlePingCountChange( newValue, oldValue ) {
  •  
  • console.log( "PingCount in isolate scope:", newValue );
  •  
  • }
  • );
  •  
  • // I listen for ping events from the parent scope chain.
  • scope.$on(
  • "ping",
  • function handlePingEvent( event, pingCount ) {
  •  
  • console.log( "Ping event in isolate scope:", pingCount );
  •  
  • // I turn around and broadcast a "received" event.
  • scope.$broadcast( "pingReceivedByIsolate" );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration. In this case we are defining an
  • // isolate scope, but not mapping any input values or functions.
  • return({
  • link: link,
  • restrict: "A",
  • scope: {}
  • });
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am a controller that is "inside" the isolate scope from an HTML markup
  • // standpoint, but not from Scope standpoint.
  • app.controller(
  • "InnerController",
  • function( $scope ) {
  •  
  • // I listen for ping-received events from the isolate scope. However,
  • // since the isolate scope is NOT in the parent-scope chain, this event
  • // should never be handlered here.
  • $scope.$on(
  • "pingReceivedByIsolate",
  • function handleIsolatePingEvent( event ) {
  •  
  • console.log( "pingReceivedByIsolate in inner controller." );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the AppController increments "pingCount" as it broadcasts the "ping" event. The isolate scope then sets up a $watch() binding for the "pingCount" and an $on() binding for the "ping" event. And, when we run this page, we get the following console output:

PingCount in isolate scope: undefined
Ping event in isolate scope: 1
Ping event in isolate scope: 2
Ping event in isolate scope: 3
Ping event in isolate scope: 4
Ping event in isolate scope: 5

As you can see, the isolate scope was able to listen for events being broadcast down through the scope hierarchy; however, it was not able to see the related scope value, "pingCount" in its parent scope.

To understand this, you have to realize that Scopes are involved in two parallel sets of relationships in AngularJS. On the one hand, there is the parent-child relationship which sets up the scope hierarchy. Then, on the other hand, there is the prototypal inheritance which determines the visibility of scope data.

In a "normal" scope, the parent and the prototype are one-in-the-same, which means that a child-scope inherits its parent-scope's data. In an "isolate" scope, the parent and the prototype are different. In an isolate scope, the prototype is Scope.prototype, not the $parent. This means that the isolate scope still has a parent - and is still part of the scope hierarchy - but that it doesn't inherit any of its parent-scope data.


 
 
 

 
 Isolate scope and scope hierarchies in AngularJS. 
 
 
 

Notice that the InnerController scope is not a child of the isolate scope; rather, they are sibling scopes that share the same parent. This is why the event being broadcast inside the isolate scope is not handled in the InnerController.

Isolate scopes force you to create more thoroughly-decoupled components, which is why I'm trying to dig into them a bit more. But, I'm still fairly new to the concept, so I'm trying to flesh out my mental model. On the one hand, its nice that they can handle broadcast events; but, on the other hand, I wonder if that breaks the concept of the isolate scope?




Reader Comments

@Yaniv,

I believe that my exploration here lines up nicely with what is being demonstrated in the article you linked. The part that makes that article very "sneaky" is the two-way binding in in the isolate scope. Since isolate scopes don't inherit data from the parent scope, you have to rely on explicitly providing scope data using attribute elements:

<div a-directive some-value="valueFromParentScope" />

In this case, we're explicitly proving "valueFromParentScope" for the isolate-scope directive. This essentially makes $parent.valueFromParentScope available in the isolate scope.valueFromParentScope.

This creates a two-way binding such that changes in the parent scope become visible in the isolate scope; and changes in the isolate scope become visible in the parent scope... which is why the "header" value in the linked article *appears* to use the isolate scope value... which is not really. It's *really* taking the value of the parent scope, which just *happens to be* reflecting the two-way binding value supplied by the isolate scope.

It's quite tricky!

While I am new to isolate scopes, I would think that you want to treat passed-in two-way bindings as *read only* such that the isolate scope is only reading from the parent scope. If the isolate scope needs to update the parent scope, I should think that that should be done exclusively in method-bindings:

scope: {
readOnlyValue: "=someAttribute",
valueMutator: "@otherAttribute"
}

In this case, the isolate scope would read from scope.readOnlyValue... but, when it wants to MUTATE the value, it should use scope.valueMutator().

But, again, I'm still learning this stuff, so maybe not the best-practice approach.

Reply to this Comment

@Yaniv,

In my previous comment, I mixed up "@" and "&" - I meant to use "&" in "&otherAttribute" to pass-through a mutator method. I believe the "@" is for read-only attribute access.

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.