Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (Feb. 2009) with: Michael Dinowitz
Ben Nadel at the New York ColdFusion User Group (Feb. 2009) with: Michael Dinowitz@mdinowitz )

Mutating Isolate Scope References In AngularJS

By Ben Nadel on

When dealing with isolate-scope variable references, in AngularJS, reading data is a non-issue. And, technically speaking, mutating data is also possible. But, just because an isolate-scope component directive can mutate a given value, it doesn't mean that it should. The more I work with AngularJS, the more convinced I am that data should only be manipulated directly by the Controller that owns it. In the context of an isolate-scope component directive, this means passing-in both the data being consumed as well the methods that can be used to alter said data.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To explore this idea, I have two different versions of an isolate-scope directive. In the first version, we'll let the isolate-scope directive mutate the passed-in collection. Then, in the second version, we'll have the isolate-scope directive "ask" its calling context to mutate the collection, on its behalf, using bound methods.

In the first version, our isolate-scope directive accepts a collection and then immediately pushes a new item onto that collection. This creates a problematic scenario in which the owner of the collection - the root Controller - isn't aware that this has happened and therefore cannot maintain its own view-model properly:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Mutating Isolate Scope References In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Mutating Isolate Scope References In AngularJS
  • </h1>
  •  
  • <p>
  • You have {{ friends.length }} friends!
  •  
  • <!-- If you are friends with Kim, that's extra awesome. -->
  • <span ng-if="includesKim">That's awesome!</span>
  • </p>
  •  
  • <!--
  • Pass the friends collection into the LIST component directive which gives it a
  • two-way data binding to the collection.
  • -->
  • <div bn-list="friends"></div>
  •  
  • <!-- This is the template for the component directive. -->
  • <script type="text/ng-template" id="list.htm">
  •  
  • <ul>
  • <li ng-repeat="item in collection">
  • {{ item }}
  • </li>
  • </ul>
  •  
  • </script>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.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 ) {
  •  
  • // Start out with THREE items in the friends collection.
  • $scope.friends = [ "Sarah", "Joanna", "Heather" ];
  •  
  • // If we are friends with Kim, that's extra cool. Of course, looking at
  • // the local collection, we know that this won't be true when at the time
  • // the Controller is instantiated.
  • $scope.includesKim = ( $scope.friends.indexOf( "Kim" ) !== -1 );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I provide a component directive for listing out items.
  • app.directive(
  • "bnList",
  • function() {
  •  
  • // Return the directive configuration object.
  • // --
  • // NOTE: We are creating an isolate scope with a two-way data binding to
  • // whatever reference is passed into the bn-list attribute.
  • return({
  • link: link,
  • scope: {
  • collection: "=bnList"
  • },
  • templateUrl: "list.htm"
  • });
  •  
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • // CAUTION: Because the isolate scope provides for a two-way data
  • // binding to passed-in scope reference, this collection can now be
  • // mutated directly by the isolate directive. This is NOT a violation
  • // of the documentation in any way; but, it is likely a violation of
  • // good practices since the "owner" of the data is not aware that
  • // these changes are being made.
  • scope.collection.push( "Kim" );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the root controller not only owns the collection, it also has a separate view-model value - includesKim - to denote whether or not a very specific friend is in the collection of friends. When the isolate-scope directive mutates the collection directly, it adds this specific friend, but the root controller is unaware of this change. As such, the "includesKim" span is never rendered:


 
 
 

 
 Mutating isolate-scope directive collections directly in AngularJS. 
 
 
 

In the next version, rather than having the isolate-scope directive mutate the collection directly, our template will bind both the collection and a mutation method. The isolate-scope directive will then ask the calling context to mutate the collection using the passed-in method:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Mutating Isolate Scope References In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Mutating Isolate Scope References In AngularJS
  • </h1>
  •  
  • <p>
  • You have {{ friends.length }} friends!
  •  
  • <!-- If you are friends with Kim, that's extra awesome. -->
  • <span ng-if="includesKim">That's awesome!</span>
  • </p>
  •  
  • <!--
  • Pass the friends collection into the LIST component directive which gives it a
  • two-way data binding to the collection. Also pass-in a method that can be used
  • to mutate said collection.
  • --
  • NOTE: The argument name being used in the addFriend() method - "item" - must
  • also be used in the isolate-scope component directive, otherwise, the value will
  • not be passed-in properly.
  • -->
  • <div bn-list="friends" add-item="addFriend( item )"></div>
  •  
  • <!-- This is the template for the component directive. -->
  • <script type="text/ng-template" id="list.htm">
  •  
  • <ul>
  • <li ng-repeat="item in collection">
  • {{ item }}
  • </li>
  • </ul>
  •  
  • </script>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.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 ) {
  •  
  • // Start out with THREE items in the friends collection.
  • $scope.friends = [ "Sarah", "Joanna", "Heather" ];
  •  
  • // If we are friends with Kim, that's extra cool. Of course, looking at
  • // the local collection, we know that this won't be true at the time the
  • // Controller is instantiated.
  • $scope.includesKim = ( $scope.friends.indexOf( "Kim" ) !== -1 );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I push a new friend onto the collection.
  • $scope.addFriend = function( newFriend ) {
  •  
  • // Validate action.
  • if ( ! newFriend ) {
  •  
  • throw( new Error( "InvalidArgument" ) );
  •  
  • }
  •  
  • $scope.friends.push( newFriend );
  •  
  • // Now that we have a new friend, there may be a chance that we can
  • // turn on the friend-specific flag.
  • $scope.includesKim = ( $scope.friends.indexOf( "Kim" ) !== -1 );
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I provide a component directive for listing out items.
  • app.directive(
  • "bnList",
  • function() {
  •  
  • // Return the directive configuration object.
  • // --
  • // NOTE: We are creating an isolate scope with a two-way data binding to
  • // whatever reference is passed into the bn-list attribute. We are also
  • // expecting a method that can be used to mutate the isolated scope
  • // reference for said collection.
  • return({
  • link: link,
  • scope: {
  • collection: "=bnList",
  • mutateCollection: "&addItem"
  • },
  • templateUrl: "list.htm"
  • });
  •  
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • // When we use the mutateCollection method to alter the bound
  • // collection, we have to use the "locals" object to bind a local
  • // value to the arguments list defined in the template. Meaning,
  • // the following key, "item", has to be the one used in the template
  • // that consumes this component directive.
  • scope.mutateCollection({
  • item: "Kim"
  • });
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Now that the data is owned and mutated by a single controller - the root Controller - the view-model is appropriately maintained. And, when the isolate-scope directive asks the calling context to change the collection, we have the necessary hooks needed to update and render the includesKim span:


 
 
 

 
 Mutating isolate-scope directive collections using bound methods in AngularJS. 
 
 
 

I'm not really a big fan of using the isolate-scope in AngularJS directives. Personally, I don't really see the advantage of it. I mean, I understand technically what it does. But, I just never feel the need for it in my own AngularJS applications. There's very little that an isolate-scope directive can do that a "normal" directive can't also do. It seems like it does nothing but add processing overhead (via implicitly bound $watch() functions).

I'm not saying that an isolate-scope directive has no value - it has a very specific value, intended for components that transclude content into the component template. But, if you're not transcluding content into a component template, then, why bother with the isolate scope?

But, I digress - the philosophy of an isolate-scope is not really relevant to this post. Mostly, I just wanted to explore the idea of isolate-scope references and what "rights" a component directives should have to its bound data. Reading data is clear; but, when it comes to mutating data, I think this is an action best performed through the calling context and bound methods.

Tweet This Groovy post by @BenNadel - Mutating Isolate Scope References In AngularJS Thanks my man — you rock the party that rocks the body!



Reader Comments

Ben, thanks for your continuing exploration of isolate scope. I've used this with some success in the past but didn't entirely grok it. I like your thinking about it & plan to incorporate your ideas in my future use of it. Thanks again!

Reply to this Comment

Hi Ben,

The first example didn't work because it is an expression, no?
It is only executed once.
If you change 'includesKim' to a function, it'll run fine.

$scope.includesKim = function() {
return $scope.friends.indexOf( "Kim" ) !== -1;
}

Reply to this Comment

@Justin,

Thanks my man - in all fairness, though, I don't use the isolate scope very much in my code, so my thinking on it is mostly from an R&D standpoint. That said, I've definitely come to the conclusion that one scope should almost never directly mutate data that is inherited (in some way) from another scope.

Reply to this Comment

@Hidenari,

Right, you are correct - the first time doesn't work because nothing re-evaluated the logic for the "includesKim" flag. Turning it into a function would fix it in this case; but, now you're doing more processing in every single pass of the digest. An indexOf() may be fast, but it's still something. Since this should be a value that we can re-calculate every time the view-model changes, it should be something we can only due from time-to-time rather than 2+ times in every digest. Of course, we can only do the one-off calculations IF the "owning" scope knows when and where the view-model is changed.

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.