AngularJS Custom-Directives scope guide

AngularJS Directive’s scope key provides us complete control over the scope of our directive element. Our goal while writing a directive should be to avoid polluting parent scope as much as possible. In this post we will discuss various scope related options/strategies including using parent scope, inheriting parent scope and creating an Isolated scope [isolating directive’s inner scope from parent scope].

This post is a part of AngularJS Directives Tutorial Series.

Isolated scope

Isolated Scope in particular is very interesting in the sense that by creating Isolated scope, directive’s inner scope can be separated from outer (parent) scope. Directive’s scope does not inherit anything from parent scope. This decoupling from parent scope is important as then directive becomes independent and can be used in different situations (reusable) without relying on specific properties/functions of parent. If directive needs some input from parent scope, that can be passed by mapping directive’s inner-scope to outer-scope [with help of attributes e.g.]

Let’s understand this whole concept with help of examples. Below mentioned code is the one from previous posts (repeated here).

<html>
	<head>  
		<title>Directive Demo</title>  
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
	</head>
	<body class="jumbotron container" ng-app="myApp">
      
		<div ng-controller="AppController as ctrl">
			<h3>List of Sale Items</h3>
			<div ng-repeat="item in ctrl.items">
				<item-widget></item-widget>
			</div>
		</div>
      
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.js">
      </script>  
      <script>
			angular.module('myApp', [])
			.controller('AppController', [function() {
              var self = this;
			  self.items = [
							{name: 'Computer', price: 500, condition:'New',brand : 'Lenovo', published:'01/11/2015'},
							{name: 'Phone', price: 200, condition:'New',brand : 'Samsung', published:'02/11/2015'},
							{name: 'Printer', price: 300, condition:'New',brand : 'Brother', published:'06/11/2015'},
							{name: 'Dishwasher', price: 250, condition:'Second-Hand',brand : 'WhirlPool', published:'01/12/2015'},
							];
			}])
			.directive('itemWidget', [function() {
				return{
					templateUrl:'saleItem.html',
					restrict: 'E'
				}
			}]);
  </script>
  </body>
</html>

And the directive’s content[saleItem.html]:

<div class="panel panel-default">
	<div class="panel-heading">
		Published at:<span ng-bind="item.published | date"></span>
	</div>
	<div class="panel-body">
			Name:<span ng-bind="item.name"></span>
			Condition:<span ng-bind="item.condition"></span>
			Price:<span ng-bind="item.price | currency"></span>
			Brand:<span ng-bind="item.brand"></span>
	</div>
</div>

The code shown above looks nice but it have one serious flaw. It is tightly coupled to parent scope. As you can see, in directive content [saleItem.html], we are using an object ‘item’ which is coming from parent scope:

	<div ng-repeat="item in ctrl.items">
		<item-widget></item-widget>
	</div>

The ‘item’ variable is coming from ng-repeat which creates a scope for each item in controller’s array. Now if we change above ng-repeat to following, our directive will break as it really looks for ‘item’ variable in it’s content [saleItem.html].

	<div ng-repeat="bla in ctrl.items">
		<item-widget></item-widget>
	</div>

To break this coupling, we have to separate the scope inside a directive from the scope outside, make it independent, and then map the outer scope to a directive’s inner scope to get any parameter we may need in directive’s content. We can do this by creating what we call an Isolated Scope. To do this, we can use a directive’s scope key:

			.directive('itemWidget', [function() {
				return{
					restrict: 'E',
					scope: {
						item: '=itemInfo'
					},
					templateUrl: 'saleItem.html',
				}
			}]);

The scope key can be passed an object [key:value pair] that contains a property for each isolated scope binding. In this case it has just one property:

  • Its name (item) corresponds to the directive’s isolate scope property ‘item’.
  • Its value (=itemInfo) tells AngularJS $compile to bind to the item-info attribute [Normalized name as for directives] in HTML. Don’t worry about that ‘=’ sign, we have lot to say about it further down in post.

Now on HTML side, we just have to map this new item-info attribute to outer’s scope.

			<div ng-repeat="bla in ctrl.items">
				<item-widget item-info="bla"></item-widget>
			</div>

Here we have mapped item-info to outer scope’s variable ‘bla’. We did not touch anything else. Directive HTML remains same as before. Now we need not to worry about name changes in outer scope as long as they are mapped to our ‘item-info’ attribute.

Live Example:

Below shown is complete code:

<html>
	<head>  
		<title>Directive Demo</title>  
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
	</head>
	<body class="jumbotron container" ng-app="myApp">
      
		<div ng-controller="AppController as ctrl">
			<h3>List of Sale Items</h3>
			<div ng-repeat="bla in ctrl.items">
				<item-widget item-info="bla"></item-widget>
			</div>
		</div>
      
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.js">
      </script>  
      <script>
			angular.module('myApp', [])
			.controller('AppController', [function() {
              var self = this;
			  self.items = [
							{name: 'Computer', price: 500, condition:'New',brand : 'Lenovo', published:'01/11/2015'},
							{name: 'Phone', price: 200, condition:'New',brand : 'Samsung', published:'02/11/2015'},
							{name: 'Printer', price: 300, condition:'New',brand : 'Brother', published:'06/11/2015'},
							{name: 'Dishwasher', price: 250, condition:'Second-Hand',brand : 'WhirlPool', published:'01/12/2015'},
							];
			}])
			.directive('itemWidget', [function() {
				return{
					restrict: 'E',
					scope: {
						item: '=itemInfo'
					},
    				templateUrl: 'saleItem.html',
				}
			}]);
  </script>
  </body>
</html>

And the directive’s content [saleItem.html] which has not been changed at all.

<div class="panel panel-default">
	<div class="panel-heading">
		Published at:<span ng-bind="item.published | date"></span>
	</div>
	<div class="panel-body">
			Name:<span ng-bind="item.name"></span>
			Condition:<span ng-bind="item.condition"></span>
			Price:<span ng-bind="item.price | currency"></span>
			Brand:<span ng-bind="item.brand"></span>
	</div>
</div>

Tip:
You can further simplify it. In main HTML, in case the attribute name you provide (attribute name is item now, before it was item-info) is same as inner-scope variable name (which is item), like:

			<div ng-repeat="bla in ctrl.items">
				<item-widget item="bla"></item-widget>
			</div>

then you can simply remove the attribute name after ‘=’.

			.directive('itemWidget', [function() {
				return{
					restrict: 'E',
					scope: {
						item: '='
					},
    				templateUrl: 'saleItem.html',
				}
			}]);

Scope provides much more…

Directive’s scope key provides much more than mentioned above. In general, scope key can have following values:

  • false [ex. scope : false or ‘no scope key defined’] Specifies that directive’s scope is SAME AS parent scope. There is no child scope. Every variables and functions from parent is available to directive and any modifications made by directive are reflected in parent. This is the default behavior.This option is not recommended.
  • true [ex. scope : true] Specifies that directive’s scope inherits the parent scope, but creates a CHILD scope of it’s own. Every variables and functions from parent is available to directive, but any modifications made by directive are not reflected in parent. This option is recommended in the sense that you will not be polluting parent scope. But do note that the moment you made a modification from inside child scope on a primitive variable which was inherited from parent scope, that particular variable’s inheritance will be totally disconnected from parent scope, and any further changes on that variable done inside parent scope will NOT be available in child scope.
  • Object [ex. scope : {}] scope option can be passed an object. This triggers AngularJS to create an Isolated scope. With Isolated scope, directive does not inherit anything from parent. Any data that parent scope needs to share with this directive needs to be passed in using attributes. This is the best approach to create reusable & independent directives. The object passed in here, contains key that refers to directive’s attributes in HTML, and the types of values that will be passed in to the directive. Essentially, We can specify three types of values that can be passed in, which AngularJS will directly put on the scope of the directive:
    • = : Specifies that value passed in directive’s attribute in HTML is an Object. It is also known as bi-directional or two-way data binding. This will be bound to directive scope and any changes done in attribute value in outer scope will be available in the directive.
    • @ : Specifies that value passed in directive’s attribute in HTML is a string, which may contain AngularJS binding expressions ({{ }}). The calculated value(if binding expression is involved) will be assigned to the directive’s scope and any changes in the value will also be available in the directive.
    • & : Specifies that value passed in directive’s attribute in HTML is a function in some controller. The directive can then trigger the function to fulfill it’s job.

Digging scope:object configuration

Let’s see the object scenario [scope : {} ] in detail with all types of options with help of an example. [Open your browser console log to see the interaction in action.]

Live Example:


Below shown is the javascript for directive.

			.directive('itemWidget', [function() {
				return{
					restrict: 'E',
					scope: {
						item: '=',
						promo: '@',
						pickMe : '&onSelect'
					},
					templateUrl: 'saleItem3.html'
				}
			}]);

First key:value pair in scope is item : ‘=’. That means in HTML, the attribute name is item and it refers to an object which is JSON [since it is defined as ‘=’], passed from parent scope.

Second key:value pair in scope is promo : ‘@’. That means in HTML, the attribute name is promo and it refers to a String [since it is defined as ‘@’], can be passed from parent scope.

Third key:value pair in scope is pickMe : ‘&onSelect’. That means in HTML, the attribute name is on-select and it refers to a function [since it is defined as ‘&’] from a specific controller.

The HTML:

		<div ng-controller="AppController as ctrl">
			<h3>List of Sale Items.</h3>
			<div ng-repeat="bla in ctrl.items">
				<item-widget item="bla" promo="Christmas-Sale" on-select="ctrl.onItemSelect(selectedItem)"></item-widget>
			</div>
		</div>

As we can see here, ‘bla’ from ng-repeat scope is a json object, assigned to item attribute. promo is assigned a simple String. on-select is assigned a function from controller which takes an argument.

Here’s the Controller’s javascript:

			.controller('AppController', [function() {
				var self = this;
				self.items = [
							{name: 'Computer', price: 500, condition:'New',brand : 'Lenovo', published:'01/11/2015'},
							{name: 'Phone', price: 200, condition:'New',brand : 'Samsung', published:'02/11/2015'},
							{name: 'Printer', price: 300, condition:'New',brand : 'Brother', published:'06/11/2015'},
							{name: 'Dishwasher', price: 250, condition:'Second-Hand',brand : 'WhirlPool', published:'01/12/2015'},
							];
				self.onItemSelect = function(name) {
					console.log('Congrats. You have just bought a', name);
				};							
			}])

Finally , the template-partial [saleItem3.html] is:

<div class="panel panel-default">
	<div class="panel-heading">
		Published at:<span ng-bind="item.published | date"></span> &nbsp;Promotion: {{promo}}<button class="pull-right" ng-click="pickMe({selectedItem:item.name})">Buy me</button>
	</div>
	<div class="panel-body">
			Name:<span ng-bind="item.name"></span>
			Condition:<span ng-bind="item.condition"></span>
			Price:<span ng-bind="item.price | currency"></span>
			Brand:<span ng-bind="item.brand"></span>
	</div>
</div>

In this template, we have one button which [thanks to ng-click directive] invokes pickMe from directive, passing an item name. What is important here is the key name ‘selectedItem’, which must match to the argument passed to the function in controller call in HTML.

Complete code:

<html>
	<head>  
		<title>Directive Demo</title>  
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
	</head>
	<body class="jumbotron container" ng-app="myApp">
      
		<div ng-controller="AppController as ctrl">
			<h3>List of Sale Items.</h3>
			<div ng-repeat="bla in ctrl.items">
				<item-widget item="bla" promo="Christmas-Sale" on-select="ctrl.onItemSelect(selectedItem)"></item-widget>
			</div>
		</div>
      
		<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.js">
		</script>  
		<script>
			angular.module('myApp', [])
			.controller('AppController', [function() {
				var self = this;
				self.items = [
							{name: 'Computer', price: 500, condition:'New',brand : 'Lenovo', published:'01/11/2015'},
							{name: 'Phone', price: 200, condition:'New',brand : 'Samsung', published:'02/11/2015'},
							{name: 'Printer', price: 300, condition:'New',brand : 'Brother', published:'06/11/2015'},
							{name: 'Dishwasher', price: 250, condition:'Second-Hand',brand : 'WhirlPool', published:'01/12/2015'},
							];
				self.onItemSelect = function(name) {
					console.log('Congrats. You have just bought a', name);
				};							
			}])
			.directive('itemWidget', [function() {
				return{
					restrict: 'E',
					scope: {
						item: '=',
						promo: '@',
						pickMe : '&onSelect'
					},
    				templateUrl: 'saleItem3.html'
				}
			}]);
		</script>
	</body>
</html>

That’s it for scope option. Let’s move to next configuration option [link function] of Directive Definition Object, which helps us define Directive’s API and functions that can then be used by directive to preform some business logic.

Next Topic : AngularJS Directive’s link option


Please note that if your AngularJS application is using templateUrl to access HTML partials, you will need an HTTP server on front to serve them. This is due to the fact that Browser’s does not allow serving or requesting files on the file:// protocol.

You can use popular Apache HTTP server to serve files. If you need assistance setting-up server,
this post can help you.

Download Source Code

References