We define a custom directive through the Module.directive function.
The reference for directives can be found here: https://docs.angularjs.org/guide/directive
In the following example we create a directive searchResult that is referenced in the HTML as search-result. The factory function returns a configuration object with three properties.
The restrict property indicates the ways in which the directive can me used. In this case: Atribute, Element, Class, coMment. If omitted, restrict defaults to AE.
The template is the HTML behind the directive, which can also be specified through a templateUrl property.
The replace property will be true if the template should replace the directive in the HTML or false if the directive element should wrap the template.
var myApp = angular.module('myApp', ['ngRoute']); myApp.config(function ($routeProvider) { ... }); myApp.controller('mainController', ['$scope', '$log', function($scope, $log) { ... }]); myApp.directive("searchResult", function() { return { restrict: 'AECM', template: '<a href="#" class="list-group-item"><h4 class="list-group-item-heading">Doe, John</h4><p class="list-group-item-text">555 Main St., New York, NY 11111</p></a>', replace: true } });
The directive could then be used in four different ways:
<div class="list-group"> <search-result></search-result> <div search-result></div> <div class="search-result"></div> <!-- directive: search-result --> </div>
Scope
By default the directive will have access to the scope of the html where it is used. To isolate the scope of the directive, we add a model through scope property of the factory object.
In the following example we have isolated the scope with an object that contains two properties. The @ symbol indicates that these properties will contain a string.
myApp.directive("searchResult", function() { return { restrict: 'AECM', templateUrl: 'directives/searchresult.html', replace: true, scope: { companyName: "@", companyAddress: "@" } } });
Values would then be passed to the directive through custom HTML attributes as shown next. In this case the values are set through interpolation using a company object in the controller’s $scope.
<search-result company-name="{{ company.name }}" company-address="{{ company.address }}"></search-result>
Within the template, the values that are being passed to the template can be accessed through interpolation as well:
<a href="#" class="list-group-item"> <h4 class="list-group-item-heading">{{ companyName }}</h4> <p class="list-group-item-text"> {{ companyAddress }} </p> </a>
Passing an Object to the Directive
If we want to pass an object to our custom directive we first have to declare a variable in the scope and assign it an equal sign string. This equal sign tells Angular to expect an object and to implement two-way binding.
myApp.directive("searchResult", function() { return { restrict: 'AECM', templateUrl: 'directives/searchresult.html', replace: true, scope: { companyObject: "=" } } });
The object would then be passed to the directive as shown next. In this case the controller’s scope contains an object assigned to a company variable. Notice we do not use the brackets for interpolation.
<search-result company-object="company"></search-result>
The company object would then be usable in the directive template. In this case, we are using interpolation to display object properties.
<a href="#" class="list-group-item"> <h4 class="list-group-item-heading">{{ companyObject.name }}</h4> <p class="list-group-item-text"> {{ companyObject.address }} </p> </a>
Passing a Function to the Directive
In the scope object we add a placeholder for a function by using a string with an ampersand:
myApp.directive("searchResult", function() { return { restrict: 'AECM', templateUrl: 'directives/searchresult.html', replace: true, scope: { companyObject: "=", formattedAddressFunction: "&" } } });
The function would then be passed to the directive as shown next. formattedAddress is a function that has been added to the controller’s $scope and we are indicating that the function takes one argument. The argument name does not have to match the name used in the function declaration.
<search-result company-object="company" formatted-address-function="formattedAddress(org)"></search-result>
To use the function within the directive we use interpolation and, instead of passing function arguments, we pass a map where the key is the argument name used in the html directive, so org maps to companyObject.
<a href="#" class="list-group-item"> <h4 class="list-group-item-heading">{{ companyObject.name }}</h4> <p class="list-group-item-text"> {{ formattedAddressFunction({ org: companyObject }) }} </p> </a>
Repeating our Custom Directive
We can combine our custom directive with ng-repeat as follows:
<search-result company-object="company" formatted-address-function="formattedAddress(org)" ng-repeat="company in companies"></search-result>
Last Minute Template Changes
Sometimes there are template decisions that can only be made at runtime, perhaps driven by the model. There are two ways of making last minute changes to the template at runtime:
- the compile function, which allows changes to the directive template when it is first read, before binding it to the scope or after binding the directive to the scope.
- the link function, which is used to make changes immediately after binding to the directive scope.
The compile function returns an object with two properties (pre and post), which hold callback functions. The use of pre is discouraged, so the following code only shows the use of post.
myApp.directive("searchResult", function() { return { restrict: 'AECM', templateUrl: 'directives/searchresult.html', replace: true, scope: { companyObject: "=", formattedAddressFunction: "&" }, compile: function(elem, attrs) { console.log('Compiling...'); //elem.removeAttr('class'); console.log(elem); return { post: function(scope, elements, attrs) { console.log('Post-linking...'); console.log(scope); if (scope.companyObject.name == 'ABC Corp') { elements.removeAttr('class'); } console.log(elements); } } } } });
The link function below is equivalent to calling the compile function without making changes to the template and returning only the post callback function. So the link function addresses the most common scenario of making format decisions after data binding in a simpler way than the compile function.
myApp.directive("searchResult", function() { return { restrict: 'AECM', templateUrl: 'directives/searchresult.html', replace: true, scope: { companyObject: "=", formattedAddressFunction: "&" }, link: function(scope, elements, attrs) { console.log('Linking...'); console.log(scope); if (scope.companyObject.name == 'ABC Corp') { elements.removeAttr('class'); } console.log(elements); } } });
Transclusion
In all the previous examples the html directive had an opening tag and a closing tag, but any HTML entered between those tags would have been ignored. There might be situations in which a custom directive should include html or text that should be displayed when the directive is rendered. To do that, we first have to add to the factory object a transclude property and set it to true (as it defaults to false).
myApp.directive("searchResult", function() { return { restrict: 'AECM', templateUrl: 'directives/searchresult.html', replace: true, scope: { companyObject: "=", formattedAddressFunction: "&" }, transclude: true } });
In our HTML element, we should enter the text that needs to be included within the directive:
<search-result person-object="person" formatted-address-function="formattedAddress(aperson)" ng-repeat="company in company"> include this text or HTML </search-result>
And in our template we need to provide a placeholder for the transcluded text or HTML:
<a href="#" class="list-group-item"> <h4 class="list-group-item-heading">{{ companyObject.name }}</h4> <p class="list-group-item-text"> {{ formattedAddressFunction({ org: companyObject }) }} </p> <small ng-transclude></small> </a>