Advanced Directive Features in AngularJS: Using Controllers in Directives

Directives can create controllers, which can then be used by other directives. This allows directives to be combined to create more complex components. To demonstrate this feature, I added a new HTML file called directiveControllers.html to the angularjs folder and used it to define the AngularJS application shown in Listing 17-3.

Listing 17-3. The Contents of the directiveControllers.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Controllers</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”productTemplate”>

<td>{{item.name}}</td>

<td><input ng-model=’item.quantity’ /></td>

</script>

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.products = [{ name: “Apples”, price: 1.20, quantity: 2 },

{ name: “Bananas”, price: 2.42, quantity: 3 },

{ name: “Pears”, price: 2.02, quantity: 1 }];

})

.directive(“productItem”, function () {

return {

template: document.querySelector(“#productTemplate”).outerText

}

})

.directive(“productTable”, function () {

return {

transclude: true,

scope: { value: “=productTable”, data: “=productData” },

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

<table class=”table table-striped” product-table=”totalValue”

product-data=”products” ng-transclude>

<tr><th>Name</th><th>Quantity</th></tr>

<tr ng-repeat=”item in products” product-item></tr>

<tr><th>Total:</th><td>{{totalValue}}</td></tr>

</table>

</div>

</div>

</body>

</html>

This example is based around two directives. The productTable directive is applied to a table element and uses transclusion to wrap a series of tr elements, one of which contains an inline binding for a value called totalValue. The other directive, productItem, is applied within the table using the ng-repeat directive to generate table rows for each of the data objects defined by the standard AngularJS controller; this isn’t the directive controller feature, just a regular one.

The result is that I have a table that contains multiple instances of the productItem directive, each of which has a two-way binding to the quantity property of the data item it represents. You can see the effect in Figure 17-4.

My goal in this section is to extend the productTable directive so that it provides a function that instances of the productItem directive can use to signal when the value in an input element changes. Since this is AngularJS, there are lots of ways that I could achieve this, but I am going to add a controller to the productTable directive and use it in the productItem directive, as shown in Listing 17-4.

Listing 17-4. Adding Support for a Directive Controller in the directiveControllers.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Controllers</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”productTemplate”>

<td>{{item.name}}</td>

<td><input ng-model=’item.quantity’ /></td>

</script>

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.products = [{ name: “Apples”, price: 1.20, quantity: 2 },

{ name: “Bananas”, price: 2.42, quantity: 3 },

{ name: “Pears”, price: 2.02, quantity: 1 }];

})

 

.directive(“productItem”, function () {

return {

template: document.querySelector(“#productTemplate”).outerText,

require: “^productTable”,

link: function (scope, element, attrs, ctrl) {

scope.$watch(“item.quantity”, function () {

ctrl.updateTotal();

});

}

}

})

.directive(“productTable”, function () {

return {

transclude: true,

scope: { value: “=productTable”, data: “=productData” },

controller: function ($scope, $element, $attrs) {

this.updateTotal = function() {

var total = 0;

for (var i = 0; i < $scope.data.length; i++) {

total += Number($scope.data[i].quantity);

}

$scope.value = total;

}

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

<table class=”table table-striped” product-table=”totalValue”

product-data=”products” ng-transclude>

<tr><th>Name</th><th>Quantity</th></tr>

<tr ng-repeat=”item in products” product-item></tr>

<tr><th>Total:</th><td>{{totalValue}}</td></tr>

</table>

</div>

</div>

</body>

</html>

The controller definition object property is used to create a controller for a directive, and the function can declare dependencies on the scope (as $scope), the element to which the directive has been applied (as $element), and the attributes on that element (as $attrs). I use the controller to define a function called updateTotal, which sums the value of the quantity properties of the data items. The require definition object property is used to declare a dependency on a controller, and I added this property to the productItem directive as follows: require:

^productTable“,

The value for the property is the name of the directive and an optional prefix, as described in Table 17-2.

I specified the name productTable (since that is the name of the directive with the controller I want to use) and prefixed the value with a, which I need to use because the productTable directive is applied to a parent of the element to which the productItem directive is applied.

I specify an additional parameter on the link function in order to use the capabilities defined by the controller, like this:

link: function (scope, element, attrs, Ctrl) {

The controller argument isn’t dependency injected, so you can call it whatever you want; my personal convention is to use the name ctrl. With these changes, I can then call the functions on the controller object as though they had been defined within the local directive:

..

ctrl.updateTotal();

I am invoking a controller method as a signal to perform a calculation, which doesn’t require any arguments, but you can pass data from one controller to another, just as long as you remember that the $scope argument passed to the controller function is the scope of the directive that defines the controller, not the scope of the directive that requires it.

2. Adding Another Directive

The value of defining controller functions comes from the ability to separate and reuse functionality without having to build and test monolithic components. In my previous example, the productTable controller has no knowledge of the design or implementation of the productItem controller, which means I can test them separately and make changes freely as long as the productTable controller continues to provide the updateTotal function.

This approach also allows you to mix and match directive functionality to create different combinations of functionality within an application, and to demonstrate this, I have added a new directive to the directiveControllers.html file, as shown in Listing 17-5.

Listing 17-5. Adding a New Directive to the directiveControllers.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Controllers</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”productTemplate”>

<td>{{item.name}}</td>

<td><input ng-model=’item.quantity’ /></td>

</script>

<script type=”text/ng-template” id=”resetTemplate”>

<td colspan=”2″><button ng-click=”reset()”>Reset</button></td>

</script>

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.products = [{ name: “Apples”, price: 1.20, quantity: 2 },

{ name: “Bananas”, price: 2.42, quantity: 3 },

{ name: “Pears”, price: 2.02, quantity: 1 }];

})

.directive(“productItem”, function () {

return {

template: document.querySelector(“#productTemplate”).outerText,

require: “^productTable”,

link: function (scope, element, attrs, ctrl) {

scope.$watch(“item.quantity”, function () {

ctrl.updateTotal();

});

}

}

})

.directive(“productTable”, function () {

return {

transclude: true,

scope: { value: “=productTable”, data: “=productData” },

controller: function ($scope, $element, $attrs) {

this.updateTotal = function () {

var total = 0;

for (var i = 0; i < $scope.data.length; i++) {

total += Number($scope.data[i].quantity);

}

$scope.value = total;

}

}

}

})

.directive(“resetTotals”, function () {

return {

scope: { data: “=productData”, propname: “@propertyName” },

template: document.querySelector(“#resetTemplate”).outerText,

require: “^productTable”,

link: function (scope, element, attrs, ctrl) {

scope.reset = function () {

for (var i = 0; i < scope.data.length; i++) {

scope.data[i][scope.propname] = 0;

}

ctrl.updateTotal();

}

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

<table class=”table table-striped” product-table=”totalValue”

product-data=”products” ng-transclude>

<tr><th>Name</th><th>Quantity</th></tr>

<tr ng-repeat=”item in products” product-item></tr>

<tr><th>Total:</th><td>{{totalValue}}</td></tr>

<tr reset-totals product-data=”products” property-name=”quantity”></tr>

</table>

</div>

</div>

</body>

</html>

The new directive is called resetTotals, and it adds a Reset button to the table that zeros all of the quantities, which it locates using data bindings on an isolated scope that provide the data array and the name of the property to set to zero. After the values are reset, the resetTotals directive calls the updateTotal method provided by the productTable directive.

This is still a simple example, but it demonstrates that the productTable neither knows nor cares which directives, if any, will use its controllers. You can create productTable instances that contain as many or as few instances of the resetTotals and productItem directives, and everything will continue to work without modification.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *