Custom Directives

Custom directives are very powerful. They enable us to create reusable parts for our application, like i.e. address-templates. With a little bit more effort we can build even whole custom html-controls. And – directives are the only place where we should manipulate the DOM!

An example of a custom (element)-directive could look like this:

<mydirective></mydirective>

Directives can be identified in three different ways:

  • By Element <mydirective>
  • By Attribute <div mydirective>
  • By Class <div class=”mydirective: expression”>

Note: When you create a directive with the default-values it matches only attributes. To make it visible in elements we need to set the restrict-property to the corresponding bold letters from above.

Directive-naming
When we create a new directive, we pass a name to it. In case it is in camel-notation, we need to write it in the html with a dash-separation. So myDirective becomes in html:

<my-directive></my-directive>

A simple directive example

Lets say we have some part in our application, which we find repeatedly over and over in our code, like i.e. a product-header. In its basic form it consists of some html with productinformation on it. We can now take this html and assign it to the template in our directive. As we would like to use the directive as element, we need to set the restrict-property to ‘E’.

The following example shows the implementation of our productheader.

Example

<div ng-controller="productController">
	<product-Header />
</div>

<script>
	angular.module('myapp', [])

		.controller('productController', ['$scope', function($scope) {
			$scope.product = {
				name: 'Super cool cam',
				type: 'x-sup-c'
			};
		}])
 
		.directive('productHeader', function() {
			return {
				restrict: 'E', // Element directive
				template: '<p>Product: {{product.name}}</p>' +
					  '<p>Type: {{product.type}}</p>'
			};
		});
</script>

Play with example »

Having the template as inline-content is not very beautiful. To solve this you can put the template-content in a separate file and set the templateurl- instead of the templateproperty. The following example demonstrates this:

.directive('productHeader', function() {
	return {
		restrict: 'E',
		templateUrl: 'productHeaderTemplate.html'
	};
});

Scope Isolation

Our example above works but there’s a serious problem with it. What if we need to show multiple product-headers on the page? As we use the global scope, all of our headers would show the same product! We can avoid this, by defining a scope-property within the directive. This creates an isolated (new separate) scope-object for our directive.

In the following example the controller provides an array of products ($scope.products), over which we loop with ng-repeat on the product-header element. Each product is passed to the directive through the product-data attribute. This productData-attribute is defined in the scope-property of the directive (scope: { product: '=productData' }).

Example

<div ng-controller="productController">
	<product-Header ng-repeat="product in products" product-data="product" />
</div>

<script>
	angular.module('myapp', [])
 
		.controller('productController', ['$scope', function($scope) {
			$scope.products = [
				{ name: 'Super cool cam', type: 'x-sup-c' },
				{ name: 'Ultra cool cam', type: 'x-ult-c' } ];
			}])
 
		.directive('productHeader', function() {
			return {
				restrict: 'E', // Element directive
				scope: { product: '=productData' }, // Defines the element attribute
				template: '<p>Product: {{product.name}}<br />Type: {{product.type}}</p>'
			};
		});
</script>

Play with example »

Scope isolation types
There are three ways, how we can isolate the scope, resulting in a very different behavior.

= This type was used in our example above. It creates a two-way binding with the object we define in the attribute. Btw, instead of describing explicitly what the attributename should be, we can define it implicitly like this:

<!-- Scope isolation -->
scope: { product: '=' } // Same like product: '=product'

<!-- Use in html -->
<product-Header product="product" />

@ An @-isolation defines a one-way binding. Instead of passing an object, we can pass an expression to the attribute. This is very useful to initialize inner parameters of a directive, like i.e. colors, fontsizes and families, labelnames etc. The @-isolation makes customizable directives possible.

<!-- Scope isolation -->
scope: { labelColor: '@' }

<!-- Use in html -->
<product-Header label-color="{{ 'blue' }}" />

& Defines a callback-handler. This sounds complicated but is easy in fact. If you have been programming in c# or java, you should know the concept of events. Callback-handlers are almost the same. They allow us to expose events, which happen inside our directive. Other developers can then register on them and react accordingly, when they’re fired.

<!-- Scope isolation -->
scope: { onHeaderClick: '&' }

<!-- Use in html -->
<product-Header on-header-click="clickedOnHeader(product)" />

To understand this better, the following example has implemented all three isolation-types we demonstrated above.

DOM manipulation in directives

As you know, we should not directly modify the DOM in AngularJS applications. The only place where we should do this, is in directives! To do this, we set the link-property to a method with the following signature:

function link(scope, element, attrs) {  }
  • Scope: Angular scope-object
  • Element: Source-element (as jqLite-object)
  • Attrs: Key-Value pair hash containing the attributes of the element

The following example outputs the data-attr attribute of the element.

Example

<div my-directive="" data-attr="This is in the attribute!" />

<script>
	angular.module('myapp', [])

	.directive('myDirective', function() {
		return {
			link: linkMethod
		};
                
		function linkMethod(scope, element, attrs)
		{
			// Note that data- is stripped away because of html5!
			element.text('Value: ' + attrs["attr"]);
		}
	});
</script>

Play with example »

Encapsulate content with transclusion

If we want to create a directive, whose inner content can be enriched by another developer, we need to implement transclusion. It consists of two pieces – a transclude-property to tell the directive how to transclude and a placeholder, which is marked by the ng-translcude-attribute in our directive-template.

transclude-options

  • transclude: true; The element gets filled with the enriching-content.
  • transclude: 'element'; The element gets replaced with the enriching-content.
Example

<!-- Directive-part of html -->
<product-Header>
	<!-- Content below is inserted into the header -->
	<p><strong>Information: </strong>This product rocks!</p>
</product-Header>

<!-- Directive-part of script -->
.directive('productHeader', function() {
	return {
		restrict: 'E',
		transclude: true,
		template: 
			'<div style="padding:15px; border: 1px solid gray;">' +
			'   <p>Product: {{product.name}}</p><p>Type: {{product.type}}</p>' +
			'   <div ng-transclude><!-- "product rocks" renders here --></div>' +
			'</div>'
		};
});

Play with example »

That is it about directives. Before you implement custom-html controls you probably want to google first, as there are already many opensource-controls available for AngularJS!