Write a post
Published Nov 21, 2016Last updated Jan 18, 2017

Build Custom Directives in Angular 2

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:

  1. Directives with templates known as Components
  2. Directives that creates and destroys DOM elements known as Structural Directives
  3. 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 and Renderer to render and re-render while structural directives use TemplateRef and ViewContainerRef 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.

Discover and read more posts from Christian Nwamba
get started
Enjoy this post?

Leave a like and comment for Christian

9
2