Angular Custom Directives Tutorial

In this Angular tutorial we learn how to create a custom attribute directive.

We cover directive behavior and dependency injection as well as NodeJS compatibility with Renderer2.

Finally we cover how to react to user events.

Here's a table of contents of what you'll learn in this lesson:
(click on a link to skip to its section)

Let's jump right in.

Lesson project setup

For this lesson there is no initial setup other than having an app.

To keep things simple, we will be working directly from inside the main ‘app.component’ component and will create whatever else we need on a step-by-step basis.

How to create a custom Attribute Directive

Angular allows us to create our own custom directives. There are a few steps we need to follow to do this, namely:

  1. Decorate a TypeScript class with the @Directive decorator and add its configuration options.
  2. Import and inject any dependencies we need based on what we want the directive to do.
  3. Register the directive.

Like with components, we can create and register a directive manually or do it in one step with the CLI.

We’ll use the CLI from inside our main app directory (C:\Angular-Projects\my-app\ , from the Environment Setup lesson).

To create a new directive, we use the directive keyword with the standard generation command.

Syntax: CLI command
ng generate directive custom_directive_name

So, let’s generate a new directive and call it ‘bgColorizer’.

Example: Command
ng generate directive bgColorizer

The command will automatically create the directive and register it in our app.module.ts file.

Two files will be created.

  1. bg-colorizer.directive.spec.ts . This is the file used to create tests. We won’t use it at the moment so it’s safe to delete it if you want to.
  2. bg-colorizer.directive.ts . This is our directive class that defines its behavior.

If you want your directives in a separate folder, add the path to the command. If the folder does not exist, it will be created automatically.

Example:
ng generate directive directives/bg-colorizer/bgColorizer

For the sake of simplicity, we will use the first option.

Custom Directive behavior and Dependency Injection

The next step is to define the behavior of our directive and import & inject any dependencies we need to make it work.

When we open the new bg-colorizer.directive.ts file, we can see that it looks almost exactly like a component file.

Because we created the directive with the CLI, Angular automatically did some stuff for us.

  • It set the selector as an attribute with a name that’s the same as the directive name, but prefixed with the word ‘app’. In our case appBgColorizer .
  • It added the postfix ‘Directive’ to the class name. In our case BgColorizerDirective . As mentioned earlier in the course, this is not required but it is a good practice.

Now let’s say we want to change the appearance of an element from inside this directive.

To do that, we need to have a reference to the native DOM-element we want to change. Angular allows us to get that reference with ElementRef .

This is where dependency injection comes in. We import ElementRef at the top of the document, then assign it in the constructor.

Dependency injection is used to provide a component with whatever they need to work.

Example: bg-colorizer.directive.ts
import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appBgColorizer]'
})
export class BgColorizerDirective {

  constructor(elRef: ElementRef) { }

}

Now we have a variable that we can use to access the element’s values through elRef.nativeElement .

Example: bg-colorizer.directive.ts
import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appBgColorizer]'
})
export class BgColorizerDirective {

  constructor(elRef: ElementRef) {
    elRef.nativeElement.style.backgroundColor = 'blue';
    elRef.nativeElement.style.color = 'white';
  }

}

In the example above, we change the background color of the element to blue and the text color to white by tapping into the style properties of the element.

All that’s left to do is to add the directive on any element we want to. For demonstration we will add the appBgColorizer attribute on a <p> tag in the main component.

Example: app.component.html
<p appBgColorizer>Lorem ipsum dolor</p>

Custom Directive NodeJS compatibility

There is no full DOM-implementation on node.js servers, so we can’t access the element directly as we did in the section above.

Instead, we need to use an abstraction-layer on the DOM called Renderer2 .

Before we can use it, we will also need to import & inject it first.

Example: bg-colorizer.directive.ts
import { Directive, ElementRef, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appBgColorizer]'
})
export class BgColorizerDirective {

  constructor(elRef: ElementRef, rend2: Renderer2) { }

}

If we want to change the appearance of an element like we did in the section above, we need to use one of Renderer2’s methods called setStyle() .

Example: bg-colorizer.directive.ts
import { Directive, ElementRef, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appBgColorizer]'
})
export class BgColorizerDirective {

  constructor(elRef: ElementRef, rend2: Renderer2) {
    rend2.setStyle(elRef.nativeElement, 'backgroundColor', 'blue');
    rend2.setStyle(elRef.nativeElement, 'color', 'white');
  }

}

It’s considered better practice to use Renderer2 instead of accessing the native elements directly.

By using this method, your directives will also be compatible with Angular Universal .

Angular Universal is a prerenderer that can generate static pages that later get bootstrapped on the client, making your app faster.

Reacting to user events

We can create a directive that reacts to user events, like mouse inputs, with the @HostListener decorator.

The @HostListener decorator is not limited to directives but can be used in components as well.

This decorator is applied to a method that gets executed when the specified event is fired.

Syntax:
@HostListener('DOM-event-name') methodName() { }

When the DOM-event-name event happens, it will execute methodName().

As an example, let’s say we want to change the color of our paragraph when a user hovers over it.

Example:
import { Directive, ElementRef, Renderer2, HostListener } from '@angular/core';

@Directive({
  selector: '[appBgColorizer]'
})
export class BgColorizerDirective {

  constructor(private elRef: ElementRef, private rend2: Renderer2) { }

  setColors(bgCol: string, txtCol: string) {
    this.rend2.setStyle(this.elRef.nativeElement, 'backgroundColor', bgCol);
    this.rend2.setStyle(this.elRef.nativeElement, 'color', txtCol);
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.setColors('blue', 'white');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.setColors('white', 'black');
  }

}

We set up two @HostListener events.

  • The ‘mouseenter’ event will execute when a user hovers over the element. It simply invokes the setColors() method to change the element’s background and text colors.
  • The ‘mouseleave’ event will execute when a user that has hovered over an element, moves the mouse away from it. The event also invokes the setColors() method to change the colors back to default.

When we run the example in the browser, everything works as expected when we hover and leave our paragraph element.