Build Custom Directives in Angular 2
Directives are the most fundamental unit of Angular applications. As a matter of fact, the most used unit, which is a component, is actually a directive. Components are high-order directives with templates and serve as building blocks of Angular applications.
How to write components in Angular 2 is everywhere on the web so we are not talking about that today. What we will be exploring today is Angular 2's directives; types, when to use them, and how to build one for our custom needs.
Types of Directives
Angular 2 categorizes directives into 3 parts:
- Directives with templates known as Components
- Directives that creates and destroys DOM elements known as Structural Directives
- Directives that manipulate DOM by changing behavior and appearance known as Attribute Directives
Components are what we have been playing with since Angular 2 was introduced so there is no need talking about it. We may go ahead and discuss the attribute and structural directives.
Attribute Directives
Attribute directives, as the name goes, are applied as attributes to elements. They are used to manipulate the DOM in all kinds of different ways except creating or destroying them. I like to call them DOM-friendly directives.
Directives in this categories can help us achieve one of the following tasks:
- Apply conditional styles and classes to elements
<p [style.color]="'blue'">Directives are awesome</p>
- Hide and show elements, conditionally (different from creating and destroying elements)
<p [hidden]="shouldHide">Directives are awesome</p>
- Dynamically changing the behavior of a component based on a changing property
Structural Directives
Structural directives are not DOM-friendly in the sense that they create, destroy, or re-create DOM elements based on certain conditions.
This is a huge difference from what hidden
attribute directive does. This is because hidden
retains the DOM element but hides it from the user, whereas structural directives like *ngIf
destroy the elements.
*ngFor
and [ngSwitch]
are also common structural directives and you can relate them to the common programming flow tasks.
Custom Attribute Directives
We had a quick look on directives and types of directives in Angular 2. And as you just found out, using the existing directives is very simple. Let's now dig a little deeper and create some to suit our own needs.
Angular 2 provides clean and simple APIs to help us create custom directives. You will find yourself creating custom attribute directives than structural directives—so let's begin with that.
Let's get started
Setup a basic Angular app using Angular Quickstart or any other method of your choice. The quickstart already comes with a simple app
component so we can just build on that. What we would do now is to create a shared folder in the app directory to hold all our custom directives and then export them using NgModule
.
myHidden: Case Study
Our first directive is going to be a case study of the existing Angular 2 hidden
directive. Let's implement that and it would serve as an eye opener of how these things work internally:
// ./app/shared/hidden.directive.ts
import { Directive, ElementRef, Renderer } from '@angular/core';
// Directive decorator
@Directive({ selector: '[myHidden]' })
// Directive class
export class HiddenDirective {
constructor(el: ElementRef, renderer: Renderer) {
// Use renderer to render the element with styles
renderer.setElementStyle(el.nativeElement, 'display', 'none');
}
}
Directives are just like other Angular 2 members created as a class. The class is then decorated with the Directive
decorator which is imported from the @angular/core
barrel.
The directive specifies a selector which is what will be looked up in our views. In this case, [myHidden]
.
In the class constructor, we use 2 DOM helpers to track the host element and render a style to it by setting the display to none.
We need to declare and export this directive via SharedModule
so that our app module can load and import it. Thereby making it available to the app, globally.
// ./app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { HiddenDirective } from './hidden.directive';
@NgModule({
declarations: [
HiddenDirective
],
exports: [
HiddenDirective
]
})
export class SharedModule{}
Now import into our AppModule
:
// ./app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// Load SharedModule
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
@NgModule({
// Import SharedModule
imports: [BrowserModule, SharedModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule{}
At this point, the directive is available for us to use anywhere in our app. Let's apply it somewhere in the app component's template:
<!--./app/app.component.html-->
<h1>Welcome</h1>
<!--This will not be shown-->
<h1 myHidden>Hidden Welcome</h1>
We are adding the directive as an attribute (myHidden
) to the markup
You can see that one title gets displayed and the other's hidden. Hidden not removed from the DOM, as the console shows. The Angular core hidden
directive could take a boolean to hide or not hide based on the value.
Let's extend ours to work the same way:
// ./app/shared/hidden.directive.ts
import { Directive, ElementRef, Input, Renderer } from '@angular/core';
@Directive({ selector: '[myHidden]' })
export class HiddenDirective {
constructor(public el: ElementRef, public renderer: Renderer) {}
@Input() myHidden: boolean;
ngOnInit(){
// Use renderer to render the emelemt with styles
console.log(this.myHidden)
if(this.myHidden) {
console.log('hide');
this.renderer.setElementStyle(this.el.nativeElement, 'display', 'none');
}
}
}
This time we are using the Input
decorator to receive value form the template and pass it down to the directive. We have to move the implementation from the constructor to ngOnInit
lifecycle method because myhidden
property will be set late. ngOnInit
will wait for all initialization processes to be complete before executing.
The attribute can now be added to our template as an input surrounded with []
and a boolean value passed to it:
<!-- ./app/app.component.html -->
<h1>Welcome</h1>
<h1 [myHidden]="val">Hidden Welcome</h1>
val
is a property on our controller:
// ./app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent{
val = true;
}
We still get the same result, but this time, we have the option to toggle the directive with a boolean.
Underline Directive
Creating an existing directive is redundant but we had to create the myHidden
directive to see how hidden
works internally. Now let's create something actually useful.
The directive we will create next will add and underline the decoration to our text on mouse over. This means that we get to see how to handle events in directives as well.
// ./app/shared/underline.directive.ts
import { Directive, HostListener, Renderer, ElementRef } from '@angular/core';
@Directive({
selector: '[myUnderline]'
})
export class UnderlineDirective{
constructor(
private renderer: Renderer,
private el: ElementRef
){}
// Event listeners for element hosting
// the directive
@HostListener('mouseenter') onMouseEnter() {
this.hover(true);
}
@HostListener('mouseleave') onMouseLeave() {
this.hover(false);
}
// Event method to be called on mouse enter and on mouse leave
hover(shouldUnderline: boolean){
if(shouldUnderline){
// Mouse enter this.renderer.setElementStyle(this.el.nativeElement, 'text-decoration', 'underline');
} else {
// Mouse leave this.renderer.setElementStyle(this.el.nativeElement, 'text-decoration', 'none');
}
}
}
In the example above, we chose not to perform any action in the constructor or a lifecycle method. We chose to write a method called hover
which is decorated with Host Listeners. The method is called by the host listeners and the host listeners are event listeners attached on the element hosting the directive.
Host Listeners are event listeners attached to any element that hosts (the directive is placed on) the directive.
We can attach the directive to our template like so:
// ./app/app.component.html
<p> <span myUnderline>Hover to underline</span> </p>
Then update SharedModule
to declare and export the directive as well:
// ./app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { HiddenDirective } from './hidden.directive';
// Import new directive
import { UnderlineDirective } from './underline.directive';
@NgModule({
declarations: [
HiddenDirective,
// Declare new directive
UnderlineDirective
],
exports: [
HiddenDirective,
// Export new directive
UnderlineDirective
]
})
export class SharedModule{}
Custom Structural Directive
Structural directives, as we have already discussed, manipulate the DOM. Such manipulations can create (not show) and destroy (not hide) DOM elements.
myIf: Case Study
Just as we re-created an existing attribute directive (hidden
) in Angular 2, let's re-create an existing structural directive (ngIf
) to see how structural directives are cooked:
// ./app/shared/if.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myIf]' })
export class IfDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) { }
@Input() set myIf(shouldAdd: boolean) {
if (shouldAdd) {
// If condition is true add template to DOM
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
// Else remove template from DOM
this.viewContainer.clear();
}
}
}
The most important difference with the way we created our attribute directive is how they are provided to the DOM. Attribute directive uses
ElementRef
andRenderer
to render and re-render while structural directives useTemplateRef
andViewContainerRef
to update the DOM content.
The directive has an Input setter that receives a boolean value. If the boolean value resolves to true, we use the ViewContainer
's createEmbeddedView
method to render the template. We can get hold of the template via the templateRef
.
When the the boolean resolves to false, we clear the ViewContainer
.
The so-called ViewContainer
, in this case, refers to the structural directive host.
You can go ahead to declare and export the directive in the SharedModule
:
// ./app/shared/shared.module.ts
// Truncated for brevity
import { IfDirective } from './if.directive';
@NgModule({
declarations: [
HiddenDirective,
UnderlineDirective,
// New directive
IfDirective
],
exports: [
HiddenDirective,
UnderlineDirective,
// New directive
IfDirective
]
})
export class SharedModule{}
Our new if
directive case study can now be applied anywhere in the app:
<!-- ./app/app.component.ts -->
<div *myIf="false">
Inside if
</div>
Attribute vs Structural: When to use which?
In cases where you need to choose either an attribute directive or a structural directive, deciding between the two might get confusing and you might end up with the wrong choice just because it seems like it solves the problem—but the solution might be limited to a certain extent.
One good example is during a situation about visibility where you are stuck between choosing whether to use the hidden
attribute directive or the ngIf
structural directive.
This simple rule can guide you when that happens—if the element that hosts the directive will still be useful in the DOM even when it is not visible, then it is a good idea to hide it than to remove it. Otherwise, the element should be removed from the DOM because it is more efficient to keep and manage fewer items on the DOM.
There is always a little price to pay, though. Hiding will keep your DOM intact and you just need toggle around. But this might also make it more complex and leave the DOM messy. Sometimes, it might even come with performance issues. Removing is cleaner but it could be expensive if the element has to be re-created in the lifetime of the application. In that case, it is up to you to apply this simple rule and make your judgement based on your application structure and behavior.
Super nice article. Just what I needed for my use case :)
How can I pass text field id value to the custom directive. And just think this custom directive is a button. When we click that button I want to open a drawer. How can i do this.
how to iterate list inside custom directive using ngFor