Author avatar

Chris Parker

Building a Custom Directive

Chris Parker

  • May 9, 2019
  • 13 Min read
  • 9 Views
  • May 9, 2019
  • 13 Min read
  • 9 Views
Languages Frameworks and Tools
Angular

Introduction

Directives are a very important feature provided by Angular. Even Angular Components are actually higher-order directives with their own template.

In this guide, we'll look into the various types of directives and then learn how to build one for our application's custom requirements.

Types of Directives

Angular has two main types of directives:

  1. Attribute
  2. Structural

Attribute Directives

Attribute directives get applied to the element's attributes. Thus, they are useful to manipulate our DOM by updating certain attributes but do not create or destroy element's as such. Thus, they can also be referred to as DOM-friendly directives. They only change the DOM element they are attached to. They mostly use data binding or event binding.

They can be useful in the following scenarios:

  • Applying styles or classes to certain elements conditionally

Example below:

1
<div [style.color]="'green'">Very excited to learn about building Custom Directives !!</div>
html
  • Showing or hiding elements conditionally

Example below:

1
<div [hidden]="showHideEl">Very excited to learn about building Custom Directives !!</div>
html
  • Changing the behavior of any component dynamically, depending on any property

Structural Directives

Structural directives, on the other hand, can create, delete, or re-create DOM elements based on certain inputs. Thus, they are generally not DOM-friendly.

Let's talk about what the "hidden" attribute directive does. It retains the element in the DOM, but only hides it from the user, while structural directives such as *ngIf remove the element from the DOM.

The other commonly used structural directives are ngFor and ngSwitch which can be used for routing programming tasks.

Custom Directive

There are many use cases in our app, where we have a custom requirement and have to create a custom directive as per our requirement. This is where we'll need to create a custom directive.

Angular has some basic APIs which can help us create custom directives. Let us first have a look at creating a custom attribute directive.

Custom Attribute Directives

Say we have a requirement where we want any element to be styled in a particular way (e.g. with a background highlight color and some text/foreground color). We can have such a text in multiple places, so we can create a custom directive for the same and reuse it across our application. So essentially, we want to be able to use our directive as below:

1
<div class="para float-left" myHighlight>Some text to be highlighted !</div>
html

To create the directive, we can run the following command using the Angular CLI:

1
ng generate directive myHighlight

The above command would automatically update the entry in our app.module.ts. However, if we create the directive manually, we'll need to update this in our AppModule as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

import { MyHighlightDirective } from './myhighlight.directive';

@NgModule({
  imports: [ BrowserModule ],
  declarations: [
    AppComponent,
    MyHighlightDirective
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
javascript

The above would inform Angular about our new directive and this is how it knows which class to invoke whenever it encounters the selector in the template.

We'll now start with the decorator. So, any directive class has to be annotated by using a @Directive decorator.

Let us create a class called "MyHighlightDirective" and then use the @Directive decorator to associate our class with the attribute "myHighlight", as shown below:

1
2
3
4
5
6
7
8
import { Directive } from '@angular/core';
//...
//...
//...
@Directive({
  selector:"[myHighlight]"
})
export class MyHighlightDirective { }
javascript

As you can see, we marked the class using the @Directive decorator and have to import the same from @angular/core.

Also, the code shown above is quite similar to how we write a component. One of the differences we can see is that our selector is wrapped inside []. This is because our selector attribute internally uses the same CSS matching rules to match any directive or component and map it to any HTML element.

Thus, if we have to select any particular element via CSS, we just write the name of the element like say div {background-color: 'green'}. And this is why, in the selector for the component in the @Component directive, we just specify the name of the component.

If we update the selector in our directive as below:

1
2
3
4
5
6
7
8
import { Directive } from '@angular/core';
//...
//...
//...
@Directive({
  selector:".myHighlight"
})
export class MyHighlightDirective { }
javascript

With the above definition, our directive would be associated with any element which has a class "myHighlight" defined like:

1
<div class="para float-left myHighlight">Some text to be highlighted !</div>
html

For now, let us associate our directive to an element with the attribute "myHighlight".

Once we have our decorator added, the next step would be to add a constructor to our directive, as shown below:

1
2
3
4
5
6
7
8
9
10
11
import { Directive } from '@angular/core';
import { ElementRef } from '@angular/core';
//...
//...
//...
@Directive({
  selector:"[myHighlight]"
})
export class MyHighlightDirective {
	constructor(private elRef: ElementRef) {}
}
javascript

With the above code, we are telling Angular to inject an instance of ElementRef into its constructor, whenever the directive is created. This is actually via dependency injection.

ElementRef is used to get direct access to the DOM element on which our directive attribute is attached to.

Let's say we now want to change the background color of this element to green and the foreground color to blue. To do that, we'll write the following code:

1
2
el.nativeElement.style.backgroundColor = "green";
el.nativeElement.style.color = "blue";
javascript

ElementRef is actually a wrapper for our actual DOM element and we access the DOM element using the property nativeElement on it.

We'll write the above code inside our ngOnInit method. So this is how our directive class would now look like;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Directive } from '@angular/core';
import { ElementRef } from '@angular/core';
//...
//...
//...
@Directive({
  selector:"[myHighlight]"
})
export class MyHighlightDirective {
	constructor(private elRef: ElementRef) {}
	ngOnInit() {
    	this.elRef.nativeElement.style.backgroundColor = "green";
		this.elRef.nativeElement.style.color = "blue";
  	}
}
javascript

The only issue with the above style of using directives is that this would assume that our app would always run in a browser. However, that might not be the case always. We can have our app running in a different environment like on a native mobile device. Thus, Angular has provided with an API which is platform independent and we can set properties on our elements using Renderer.

This is how our class would get updated to if we use the Renderer helper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Directive } from '@angular/core';
import { Renderer } from '@angular/core';
//...
//...
//...
@Directive({
  selector:"[myHighlight]"
})
export class MyHighlightDirective {
	constructor(private elRef: ElementRef, private renderer: Renderer) {}
	ngOnInit() {
		this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'green');
		this.renderer.setStyle(this.elRef.nativeElement, 'color', 'blue');    	
  	}
}
javascript

The "setStyle" method has the following signature:

1
setStyle(element: any, style: string, value: any, flags?: RendererStyleFlags2): void  
javascript

Thus, we are now updating our DOM element's style (background and foreground color) via the "Renderer" and not directly accessing our element, which is a good practice.

We are now applying a hardcoded background and foreground colors to the DOM element. Let's say that we want that to be configurable, meaning our DOM element can say that it needs the background color and foreground color to be something specific. Thus, we want to be able to say:

1
<div class="para float-left" myHighlight [backgroundColor]="'black'" [foregroundColor]="'white'">Some text to be highlighted !</div>
html

The above can be achieved with the help of @Input decorator. We'll also use the @HostBinding decorator to update the styles on our element. Let us update our class showing the use of @Input and @HostBinding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Directive } from '@angular/core';
import { Input } from '@angular/core';
import { HostBinding } from '@angular/core';
//...
//...
//...
@Directive({
  selector:"[myHighlight]"
})
export class MyHighlightDirective {
	@Input() backgroundColor:string = 'green';
	@Input() foregroundColor:string = 'blue';
	@HostBinding('style.backgroundColor') bgColor:string;
	@HostBinding('style.color') color:string;  
	constructor() {}
	ngOnInit() {
		this.bgColor = this.backgroundColor;		
		this.color = this.foregroundColor;
  	}
}
javascript

If you see the above code, we now have default values for the background and foreground colors (i.e. green and blue). However, if the values are passed to the directive via data binding, we instead use those values to set the styles on our element.

We can also attach event listeners to our directive. Say, we want our text to have a different background and foreground colors when the user hovers over it. Thus, we want to be able to pass this to our directive:

1
2
3
4
5
<div class="para float-left" myHighlight 
	[backgroundColor]="'black'" 
	[foregroundColor]="'white'"
	[hoverBackgroundColor]="'yellow'"
	[hoverForegroundColor]="'red'">Some text to be highlighted !</div>
html

To be able to do this, we'll need to use the @HostListener decorator as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Directive } from '@angular/core';
import { Input } from '@angular/core';
import { HostBinding } from '@angular/core';
import { HostListener } from '@angular/core';
//...
//...
//...
@Directive({
  selector:"[myHighlight]"
})
export class MyHighlightDirective {
	@Input() backgroundColor:string = 'green';
	@Input() foregroundColor:string = 'blue';
	@Input() hoverBackgroundColor:string = 'gray';
	@Input() hoverForegroundColor:string = 'orange';
	@HostBinding('style.backgroundColor') bgColor:string;
	@HostBinding('style.color') color:string;  
	constructor() {}
	ngOnInit() {
		this.bgColor = this.backgroundColor;		
		this.color = this.foregroundColor;
  	}
	@HostListener('mouseenter') onMouseEnter(eventData: Event) {
		this.bgColor = this.hoverBackgroundColor;		
		this.color = this.hoverForegroundColor;				    	
  	}
}
javascript

Thus, host listeners are just event listeners that get attached to the DOM element which is hosting our directive.

Custom Structural Directive

Now that we have an understanding of the custom attribute directive, let us have a quick look at how we can create a custom Structural Directive. We can try recreating the in-built ngIf directive. Let us call that as "myCustomIf". This is how the class for that would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Directive} from '@angular/core';
import { Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ 
	selector: '[myCustomIf]' 
})
export class myCustomIfDirective {
  constructor(
    private template: TemplateRef<any>,
    private container: ViewContainerRef
    ) { }

  @Input() set myIf(shouldAddToDOM: boolean) {
    if (shouldAddToDOM) {
      // If the value is true, add template to the DOM
      this.container.createEmbeddedView(this.template);
    } else {
     // Otherwise delete template from the DOM
      this.container.clear();
    }
  }

}  
javascript

We can now use this directive anywhere in our app. To do that, we'll just write;

1
2
3
4
<!-- /app/my.component.html -->
<div *myCustomIf="false">
    Inside if
</div>
html

Conclusion

Thus, we can see that directives can be really useful to implement any sort of custom logic on our DOM element. While a lot of common functionalities can be achieved with the help of inbuilt directives, it is not uncommon to have custom requirements in any app and in such cases, we can always write our own custom directive to get the desired result. Also, we can write both the types of directive i.e. Attribute directive and Structural directive.

1