Skip to main content
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Joe Mesot
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Joe Mesot

Mutating Isolate Scope References In AngularJS

By
Published in Comments (4)

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.

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

Reader Comments

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

1 Comments

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;
}

15,848 Comments

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

15,848 Comments

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

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