Skip to main content
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Nick Carter and Jessica Kennedy
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Nick Carter Jessica Kennedy ( @mistersender )

Using Isolate Scope In Directives In AngularJS

By on

The other day, in my AngularJS "code smell" blog post about directives, I stated that the content may not apply to directives that use the "isolate" scope. The truth is, I don't know too much about the isolate scope feature since I never really understood the use-case. But, I'm tired of not knowing; so, I'm going to start digging into it. This blog post is mostly for me (I think better when I write) - my first AngularJS directive that uses the isolate scope.

Run this demo in my JavaScript Demos project on GitHub.

The point of the isolate scope, from what I have read, is to maintain a bubble around the directive that prevents it from accidentally reading from or writing to the parent $scope chain. Communication through this bubble wall can only be done through explicitly defined openings in the isolate scope configuration. This configuration maps directive-local scope properties to parent-scope expression evaluation or bi-directional parent-scope property bindings; both, of which, are defined using element attributes on the DOM (Document Object Modal) tree.

The isolate scope configuration can also map Element attribute values onto directive scope bindings. But, as of this writing, I am not sure how (if at all) this differs from the Attributes collection, which is still available inside of an isolate scope directive. It seems the only difference would be that one is monitored using scope.$watch() and the other is monitored using attributes.$observe(). But, that's a blog post for another day.

To experiment with this idea, I've created an isolate scope directive which tracks mouse-down events on the document. The goal here is to evaluate an expression on the parent scope (using isolate scope bindings) when the user mouses-down outside of the directive element:

bn-mousedown-outside="doSomething()"

But, in order to make this a more meaty exploration, I also want to make use of the property and attribute bindings as well. As such, you can also enabled and disable the directive using a scope property and tell it to ignore mouse-down events inside elements based on CSS selectors:

ignore-mousedown-if="[ flag for enabled/disabled ]"
ignore-mousedown-inside="[ css selectors to ignore ]"

Ok, let's look at some code:

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Using Isolate Scope In Directives In AngularJS
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		Using Isolate Scope In Directives In AngularJS
	</h1>

	<p class="actions">
		<a ng-click="showMessage()">Show Message</a>
		&nbsp;|&nbsp;
		<a ng-click="hideMessage()">Hide Message</a>
	</p>

	<!--
		When this message shows, we are going to use an "outside" mousedown event
		binding to know when to close it. This is a common use-case in popups, modals,
		and dropdowns.

		However, we don't want to indiscriminantly handle the mousedown event; we want
		to ingore certain DOM elements (like the container) as well as certain conditions
		(ie, when the handler is disabled by the parent controller).

		ignoreMousedownIf : Scope property that determins if directive is enabled.
		ignoreMousedownInside : CSS selectors for "safe" elements.
	-->
	<p
		ng-if="isShowingMessage"
		bn-mousedown-outside="hideMessage()"
		ignore-mousedown-if="shouldIgnoreMousedown"
		ignore-mousedown-inside="p.actions , h1"
		class="message">

		I'm sorry, I can't hear you over the awesomeness of this message!

		[
			<span ng-hide="shouldIgnoreMousedown">
				Enabled &mdash;
				<a ng-click="disableClickDetection()">Disable click detection</a>
			</span>

			<span ng-show="shouldIgnoreMousedown">
				Disabled &mdash;
				<a ng-click="enableClickDetection()">Enable click detection</a>
			</span>
		]

	</p>


	<!-- 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.16.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 ) {

				// I determine whether or not we're showing the demo message.
				$scope.isShowingMessage = false;


				// I determine whether or not we want to actually show the
				$scope.shouldIgnoreMousedown = false;


				// ---
				// PUBLIC METHODS.
				// ---


				// I disable click detection. The message can only be closed through
				// explicit calls to teh hideMessage() method.
				$scope.disableClickDetection = function() {

					$scope.shouldIgnoreMousedown = true;

				};


				// I enable click detection. The message can be hidden through mousedown
				// events located outside of the message.
				$scope.enableClickDetection = function() {

					$scope.shouldIgnoreMousedown = false;

				};


				// I hide the demo message.
				$scope.hideMessage = function() {

					$scope.isShowingMessage = false;

				};


				// I show the demo message.
				$scope.showMessage = function() {

					$scope.isShowingMessage = true;

				};

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I provide hooks to mouse-down events on the document that take place outside
		// of the current element.
		app.directive(
			"bnMousedownOutside",
			function( $document ) {

				// I bind JavaScript events to the directive scope.
				function link( $scope, element, attributes ) {

					// In the isolate-scope configuration, the external scope property,
					// [ignoreMousedownIf] was mapped to local scope property
					// [isDisabled]. However, if the attribute doesn't exist, the local
					// scope value will be undefined. As such, we are defining a default
					// value in the $watch expression.
					$scope.$watch(
						"! ( isDisabled || false )",
						function( newValue, oldValue ) {

							// If enabled, listen for mouse events.
							if ( newValue ) {

								$document.on( "mousedown", handleMouseDown );

							// If disabled, but previously enabled, remove mouse events.
							} else if ( oldValue ) {

								$document.off( "mousedown", handleMouseDown );

							}

						}
					);

					// When the local scope is destroyed, be sure to clean up the event
					// bindings on the document.
					$scope.$on(
						"$destroy",
						function() {

							$document.off( "mousedown", handleMouseDown );

						}
					);


					// I handle the mouse-down events on the document.
					function handleMouseDown( event ) {

						// Check to see if this event target provides a click context
						// that should be ignored.
						if ( shouldIgnoreEventTarget( $( event.target ) ) ) {

							return(
								console.warn( "Ignoring mouse-down event.", ( new Date() ).getTime() )
							);

						}

						// Even though this directive is isolated, we still need to call
						// $apply() to tell AngularJS that a change has happened. The
						// $digest mechanism can still be triggered from an isolated
						// scope.
						$scope.$apply(
							function() {

								$scope.callback();

							}
						);

					}


					// I detemine if the given mousedown context should be ignored.
					function shouldIgnoreEventTarget( target ) {

						// If the click is inside the parent, ignore.
						if ( target.closest( element ).length ) {

							return( true );

						}

						// If the click is inside the "exception" CSS selectors
						// (if provided), then ignore.
						// --
						// NOTE: Demo assumes that attribute value does not use
						// interpolation and therefore will not have to be watched.
						if (
							$scope.exceptionSelectors &&
							target.closest( $scope.exceptionSelectors ).length
							) {

							return( true );

						}

						// If there is no need to ignore the target at this point, let
						// the event be processed.
						return( false );

					}

				}


				// Return the directive configuration for scope isolation.
				return({
					link: link,
					scope: {
						callback: "&bnMousedownOutside",
						exceptionSelectors: "@ignoreMousedownInside",
						isDisabled: "=ignoreMousedownIf"
					}
				});

			}
		);

	</script>

</body>
</html>

In this case, because I wrote both the demo and the directive, I know that all of the various scope properties and DOM tree attributes will exist. As a directive author, however, you don't. As such, your isolate scope mappings won't always correspond to actual values. In such cases, the directive scope properties will show up as "undefined."

NOTE: The mapped property keys still exist in the scope; but, the value of the property is "undefined."

You may notice that when I invoke the callback, inside of the isolate scope, I am using the normal $apply() method to tell AngularJS about potential model changes. Even though this scope is outside of the normal scope chain, the $apply() method still works in the greater context of the application. This is because the $apply() method triggers a $digest on the $rootScope, regardless of where the $apply() call was initiated.

As this is my first isolate scope directive, I find it all very interesting. But, I am not sure that I fully realize the benefits at this time. I do like that the scope bindings force you to explicitly define all of your communication channels. But the same could be done with "best practice" conventions. There's still a lot more to explore, however, so I will try to defer any judgement until I have a more well-rounded understanding.

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

Reader Comments

27 Comments

On a previous project I used to create one-off directives with isolated scopes instead of using ng-include. I only passed to them what was needed and it completely broke scope inheritance... on purpose. To me scope inheritance always felt dangerous and risky. If you use something inherited from scope, you are guaranteeing that you've broken encapsulation.

I'm not sure if I even knew the benefits when I did it, but it made refactoring amazingly easy. I also used a similar technique instead of ng-switch/ng-include combo which had the side effect of making the 1.08 to 1.2+ switch way easier because of the transclusion changes.

15,674 Comments

@Jonathan,

I definitely like that it the isolate approach really pushes the develop to think about encapsulation. I'm not opposed to scope inheritance, in general, and I think it definitely makes sense in the Controllers. But, I am kind of digging on the way scope isolation forces you to define HTML attribute hooks instead of relying on scope methods. This way, you can have different scope method names pipe into the same directive in different contexts. That's definitely a win.

1 Comments

I realize that this was posted many months ago and you have probably figured this out by now, but the value of isolate scopes comes in when you reuse directives(which is really what they're intended for).
Let's say you want to create a reusable directive that says "Hello, Username" whatever the users name is. Sure you could just grab $scope.userName off the controllers scope, but if you're writing this directive for multiple developers, you don't know what variable that they're using in their controller, so you have them pass it in a property. Maybe one developer called it $scope.name and another one $scope.firstName. so your html for that directive just looks like this...
<hello username="firstName"><hello>
and for the other developer they'd use
<hello username="name"><hello>
Obviously a super simple useless example, but hopefully that will help someone out.

15,674 Comments

@Joe,

From what I've seen, isolate scopes don't necessarily relate to reusability at all. There's also nothing about an isolate scope that facilitates passing values in through attributes - you can easily pass both values and function references in through attributes and reference and invoke them, respectively, in non-isolate scopes.

From everything that I've been looking into, it seems the only true value of the isolate scope is when you are creating a directive that *transcludes* content AND provides *additional HTML* (to wrap the transcluded content). That way, the additional HTML can use its own isolated scope.

Outside of that, I wouldn't use isolate scope by default. In fact, doing so would lead to actual problems in an app:

www.bennadel.com/blog/2729-don-t-blindly-isolate-all-the-scopes-in-angularjs-directives.htm

I'm not trying to push back against the isolate scope - it serves a purpose. I'm only saying that the purpose is very specific and shouldn't be seen a general use-case.

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