AngularJS Custom-Directives controllers, require option guide

AngularJS Custom Directive’s can have controllers. Controllers in Directive’s are used for inter-directive communication. This post discusses Directive’s controller, require option and controller argument in directive’s link function.

This post is a part of AngularJS Directives Tutorial Series.

Difference between Directive’s link function & Directive’s controller

In previous posts, we have discussed link function. link function are specific to directive instance and can be used to define directive’s behavior & business logic. Controller in directive's on the other hand are used for Directive’s inter-communication. That means, one directive on an element wants to communicate with another directive [on the same element or on parent hierarchy]. This includes sharing state or variables, or even functions.

Let’s see this with help of a live example. Play with it to see it in action. This example is built-upon example from previous posts. Complete code is available at the end of post.

Live Example:

In our example, we have one top-level directive named shoppingWidget which contains different items for sale and a shopping cart. Each Sale item itself is implemented as a directive named itemWidget. Each time an item is clicked, we add that item to shopping cart.

Below shown is itemWidget directive.

			.directive('itemWidget', [function() {
				return{
					restrict: 'E',
					replace:true,
					transclude:true,
					require : '^shoppingWidget',
					scope: {
						item: '=',
						promo: '@',
						pickMe : '&onSelect'
					},
    				templateUrl: 'shopItem.html',
					link : function(scope, element, attrs, cartCtrl){
					    //Add event listener for 'click' event					
						element.on('click', function(event) {
						
								element.html('Thanks for buying this item.');
								element.css({
									color: 'green'
								});
								cartCtrl.addItemToCart(scope.item);//Add item to Shopping Cart.
								scope.$apply();
						  });
					}
				}
			}]);

Comparing to previous posts, there are two new concepts used in this snippet : require option & Controller argument passed in link function. Both concepts are related with each other.

1. require option



First of all, notice the require option in directive definition. It says to AngularJS that in order to fulfill it’s job, itemWidget requires another directives to be present in HTML. In other words, require option specifies the dependencies. A directive can be dependent on more than one directives.

  • require : ‘shoppingWidget’ : Specifies that there is only one dependency.
  • require : ['shoppingWidget','ngModel'] : Specifies that there are two dependencies. For multiple dependencies, we use array syntax.

If required directive [on which our directive depends on] found successfully by AngularJS , that directive’s controller will be available as the 4th argument in our directive’s link function. In case our directive is dependent on more that one directive’s, then link function gets an array of controllers as 4th argument. We will come to link function in a moment.

require option sign:

Sign preceding required directive-name provides extra meaning to AngularJS, it is explained below in detail:

  • require: ‘shoppingWidget’ : It specifies that a directive shoppingWidget must be present on the element the current directive is applied on. If it is not found, AngularJS will throw an exception.
  • require: ‘^shoppingWidget’ : It specifies that a directive shoppingWidget must be present on the parent hierarchy [not necessarily immediate parent] of the element the current directive is applied on. If it is not found, AngularJS will throw an exception.
  • require: ‘?shoppingWidget’ : It specifies that a directive shoppingWidget is an optional dependency. If it is not found on the same element, no exception will be thrown. But then AngularJS will pass null as 4th argument to the link function. Don’t forget to null-check before using the controller in this case.
  • require: ‘?^shoppingWidget’ : It specifies that a directive shoppingWidget is an optional dependency. If it is not found on the parent hierarchy of the current element this directive is applied on, no exception will be thrown. But then AngularJS will pass null as 4th argument to the link function. Don’t forget to null-check before using the controller in this case.

2. Controller argument in link function



Controller argument passed in link-function of a directive is the controller of the directive referred in required option. If there are more than one directives specified in require option , like require : ['shoppingWidget','ngModel'], then link function gets an array of controllers as 4th argument.

link : function(scope, element, attrs, cartCtrl){
					    //Add event listener for 'click' event					
						element.on('click', function(event) {
						
								element.html('Thanks for buying this item.');
								element.css({
									color: 'green'
								});
								cartCtrl.addItemToCart(scope.item);//Add item to Shopping Cart.
								scope.$apply();
						  });
					}

In our example, when an element gets clicked, we are calling a function on the passed controller [ cartCtrl.addItemToCart(scope.item);] and passing it the clicked item. The passed controller belongs to another directive. This way, we are able to communicate from one directive [itemWidget] to another directive [ shoppingWidget]. Now that directive can perform some business logic or do some special action.
Communication between directives is the core of controller function passed to link function.

3. Directive’s Controller



As we know from above, Directive’s can include controllers which can then be used for inter-directive communication. Below shown is the shoppingWidget Directive used in our example:

			.directive('shoppingWidget', [function() {
				return{
					restrict: 'E',
					transclude: true,
					templateUrl:'shopDetails.html',
					scope: true,//Define a new Scope for this directive so that local variables don’t override anything in the parent scope.
					controller: ['$scope', function($scope) {//$scope refers to scope of this perticular directive instance.
						$scope.cart = [];//Now cart can be accessed as 'cart' in directive's template.
						var self = this;	
						self.addItemToCart = function(item) {//This function can be accessed by child or sibling controllers.
						//This function will not be accessible from directive's template as it is not defined on scope.
							$scope.cart.push(item);
						};
					}]
				}
			}])

There are several interesting things going on here.

  • Firstly, we have defined our scope using scope : true. It defines a new Scope for this directive so that local variables from this directive don’t override anything in the parent scope (whichever it may be). It’s a good practice.
  • Next, we have defined a controller, using same array syntax we saw in previous posts. In this case, it is injected with $scope. Thanks to scope:true, $scope will refer to directive’s own scope, and not polluting parent scope. Anything defined on $scope can directly be accessed in directive’s template/HTML.
  • We have defined a function addItemToCart in controller’s instance using this. That means this function can be accessed by other components if they have access to this controller. Note that this function can not be used in directive’s template/HTML as this is not defined on $scope of directive.
  • Finally, we simply push the item passed, to cart array defined on $scope, which can then be available in directive’s HTML/template.
  • Take special note that we have also included transclude:true. Reason being that this directive acts as a container which will include other directives like itemWidget.Below shown is it’s template file [shopDetails.html]

    <div>
    	<!--Display Shopping Cart -->
    	<div class="panel panel-success">
    		<div class="panel-heading">
    			Your Shopping Cart.
    		</div>
    		<div class="panel-body">
    			<ul class="list-group">
    				<li class="list-group-item" ng-show="cart.length > 0">
    					Total Items
    					<span class="badge">{{cart.length}}</span>
    				</li>
    				<li class="list-group-item" ng-hide="cart.length > 0">
    					Your cart is Empty.
    				</li>
    			</ul>
    			<ul class="list-group">
    				<li class="list-group-item" ng-repeat="item in cart">
    					{{item.name}} with Price {{item.price}} Quantity : 
    				</li>
    			</ul>
    		</div>
    	</div>
    	<!--Include Items on sale -->
    	<div ng-transclude></div>
    </div>
    
    

    First child div represents a simple shopping-cart, using ng-show/ng-hide to show/hide perticular messages, and a simple loop on $scope.cart using ng-repeat to display the item being added.

    Second child div is shown with ng-transclude. This div at runtime will contain the content of the child element of shopping-widget[from HTML].

    Below shown is the main HTML:

    <div ng-controller="AppController as ctrl">
                <shopping-widget> <!--Acting as parent container-->
                    <div class="panel panel-success">
                        <div class="panel-heading">
                            Popular Items on Sale.
                        </div>
                        <div class="panel-body">
                            <div ng-repeat="bla in ctrl.items">
                                <item-widget item="bla" promo="Christmas-Sale" on-select="ctrl.onItemSelect(selectedItem)">
                                    Last item left. Hurry up. 
                                </item-widget>
                            </div>
                        </div>
                    </div>
                </shopping-widget>
            </div>
    

    And the itemWidget’s template/HTML :

    shopItem.html

    <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">
    			<div class="alert alert-info" ng-transclude></div>
    			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>
    

    That’s it. This should be enough to get started with writing your own directive’s. As always, please share your thoughts and improve our collective learning process.

    Interesting Notes:

    • In case you do not have require key defined in your directive, but your link function contains controller as 4th argument, in that case the controller will refer to your directive’s own controller, or undefined if there is no controller defined in your directive.
    • You CAN require your own directive. In that case, the 4th argument will refer to your directive’s own controller or undefined

    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">
    			<shopping-widget>
    				<div class="panel panel-success">
    					<div class="panel-heading">
    						Popular Items on Sale.
    					</div>
    					<div class="panel-body">
    						<div ng-repeat="bla in ctrl.items">
    							<item-widget item="bla" promo="Christmas-Sale" on-select="ctrl.onItemSelect(selectedItem)">
    								Last item left. Hurry up. 
    							</item-widget>
    						</div>
    					</div>
    				</div>
    				
    			</shopping-widget>
    		</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('shoppingWidget', [function() {
    				return{
    					restrict: 'E',
    					transclude: true,
    					templateUrl:'shopDetails.html',
    					scope: true,//Define a new Scope for this directive so that local variables don’t override anything in the parent scope.
    					controller: ['$scope', function($scope) {//$scope refers to scope of this perticular directive instance.
    						$scope.cart = [];//Now cart can be accessed as 'cart' in directive's template.
    						var self = this;	
    						self.addItemToCart = function(item) {//This function can be accessed by child or sibling controllers.
    						//This function will not be accessible from directive's template as it is not defined on scope.
    							$scope.cart.push(item);
    						};
    					}]
    				}
    			}])
    			.directive('itemWidget', [function() {
    				return{
    					restrict: 'E',
    					replace:true,
    					transclude:true,
    					require : '^shoppingWidget',
    					scope: {
    						item: '=',
    						promo: '@',
    						pickMe : '&onSelect'
    					},
        				templateUrl: 'shopItem.html',
    					link : function(scope, element, attrs, cartCtrl){
    					    //Add event listener for 'click' event					
    						element.on('click', function(event) {
    						
    								element.html('Thanks for buying this item.');
    								element.css({
    									color: 'green'
    								});
    								cartCtrl.addItemToCart(scope.item);//Add item to Shopping Cart.
    								scope.$apply();
    						  });
    					}
    				}
    			}]);
    		</script>
    	</body>
    </html>
    

    shopItem.html

    <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">
    			<div class="alert alert-info" ng-transclude></div>
    			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>
    

    shopDetails.html

    <div>
    	<!--Display Shopping Cart -->
    	<div class="panel panel-success">
    		<div class="panel-heading">
    			Your Shopping Cart.
    		</div>
    		<div class="panel-body">
    			<ul class="list-group">
    				<li class="list-group-item" ng-show="cart.length > 0">
    					Total Items
    					<span class="badge">{{cart.length}}</span>
    				</li>
    				<li class="list-group-item" ng-hide="cart.length > 0">
    					Your cart is Empty.
    				</li>
    			</ul>
    			<ul class="list-group">
    				<li class="list-group-item" ng-repeat="item in cart">
    					{{item.name}} with Price {{item.price}} Quantity : 
    				</li>
    			</ul>
    		</div>
    	</div>
    	<!--Include Items on sale -->
    	<div ng-transclude></div>
    </div>
    

    That’s it. This should be enough to get started with writing your own directive’s. As always, please share your thoughts and improve our collective learning process.


    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

    If you like tutorials on this site, why not take a step further and connect me on Facebook , Google Plus & Twitter as well? I would love to hear your thoughts on these articles, it will help me improve further our learning process.

    If you appreciate the effort I have put in this learning site, help me improve the visibility of this site towards global audience by sharing and linking this site from within and beyond your network. You & your friends can always link my site from your site on www.websystique.com, and share the learning.

    After all, we are here to learn together, aren’t we?