Skip to content

Angular Directives

Aakash Goplani edited this page Aug 6, 2019 · 34 revisions

What are Directives

  • Directives are set of instructions for DOM. Whenever Angular renders a directive, it changes the DOM according to the instructions given by the directive. Directive appears within an element tag similar to attributes.

  • The Directives can be written in both UpperCamelCase and lowerCamelCase. This is because NgIf refers to the directive class & ngIf refers to the directive’s attribute name. When we talk about its properties and what the directive does, we will refer to the directive class. Whereas we will refer to the attribute name when describing how you apply the directive to an element in the HTML template.

  • The difference between a component and a directive in Angular 2 is that a component is a directive with a view whereas a directive is a decorator with no view. A component is also a directive-with-a-template. A @Component decorator is actually a @Directive decorator extended with template-oriented features.

This wiki covers following sections:


Built-in Directives

Components directives

  • These form the main class having details of how the component should be processed, instantiated and used at runtime.
  • It is decorated with the @component decorator. E.g.
@Component({
    selector: 'app-component',
    templateUrl: './app.component.html',
    styles: ['./app.component.css']
})

Structural directives

  • The structural directive is used in manipulating the Dom Layout, typically by adding, removing elements.

  • Structural directives are always appended with an asterisk * so that Angular recognizes that it may expect a change in structure of DOM. Internal working of *.

  • Its built-in types are *ngIf, *ngFor, *ngSwitch.

  1. *ngIf:

    • It takes a boolean expression and makes an entire chunk of the DOM appear or disappear. You can assume it similar as if statement in a programming language
    • The ngIf directive doesn’t hide elements. It adds and removes them physically from the DOM. You can confirm it by using browser developer tools to inspect the DOM. When the condition is false, NgIf removes its host element from the DOM, detaches the component from Angular change detection, and destroys it.
    • Syntax: <div *ngIf="movie" >{{movie.name}}</div>
  2. *ngFor

    • NgFor is a repeater directive - a way to present a list of items. You define a block of HTML that defines how a single item should be displayed and then you tell Angular to use that block as a template for rendering each item in the list.
       <tr *ngFor="let serverInstance of serversCollection">
          <td>{{serverInstance.id}}</td>
          <td>{{serverInstance.name}}</td>
       </tr>
    • The string assigned to *ngFor is not a template expression. Rather, it's a microsyntax - a little language of its own that Angular interprets. The string "let item of items" means: Take each item in the items array, store it in the local item looping variable, and make it available to the templated HTML for each iteration.
    • *ngFor with index: The index property of the NgFor directive context returns the zero-based index of the item in each iteration. You can capture the index in a template input variable and use it in the template.
    • <div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div>
    • When working with large index list - use *ngFor trackBy
  3. *ngSwitch

    • NgSwitch is like the JavaScript switch statement. It displays one element from among several possible elements, based on a switch condition. Angular puts only the selected element into the DOM.

    • NgSwitch is actually a set of three, cooperating directives: NgSwitch, NgSwitchCase, and NgSwitchDefault as in the following example.

    <ol [ngSwitch]="serverStatus">
       <li *ngSwitchCase="'Online'">Online</li>
       <li *ngSwitchCase="'Offline'">Offline</li>
       <li *ngSwitchDefault>Unknown</li>
    </ol>
    • NgSwitch is the controller directive. Bind it to an expression that returns the switch value, such as feature. It is a attribute directive.
    • NgSwitchCase adds its element to the DOM when its bound value equals the switch value and removes its bound value when it doesn't equal the switch value. It is a structural directive.
    • NgSwitchDefault adds its element to the DOM when there is no selected NgSwitchCase. It is a structural directive.

Attribute directives

  • Attribute Directives deal with changing the look and behavior of the dom element.

  • ngClass, ngStyle are the most used attribute directives.

  1. ngClass

    • Add or remove several CSS classes simultaneously with ngClass.
    • The expression passed on to ngClass can be:
    <!--Original Button Styling that can be replace by ngClass :-->
    <button class="btn btn-primary">Button</button>
    
    <!--Passing an Array of classes:-->
    <button [ngClass]="['btn', 'btn-primary']">Button</button> 
    
    <!--Passing a String:-->
    <button [ngClass]="'btn btn-primary'">Button</button>
    
    <!--Passing an Object:-->
    <button [ngClass]="{ btn:true, 'btn-primary':true }">Button</button>
    • Note: if more than one word property is used, e.g. btn-primary, we wrap it using quotations.
  2. ngStyle

    • Use NgStyle to set many inline styles simultaneously and dynamically, based on the state of the component
    • Without NgStyle
       <div [style.font-size]="isSpecial ? 'x-large' : 'smaller'">This div is x-large or smaller.</div>
    • However, to set many inline styles at the same time, use the NgStyle directive
    <div [ngStyle]="currentStyles">This div is initially italic, normal weight, and extra large (24px).</div>
    • You can also add a JS object within ngStyle while using in-build css properties in camel-case e.g.
       <div [ngStyle]="{color: 'red'}">This div is special</div>
       <div [ngStyle]="{'background-color': person.country === 'IN' ? 'green' : 'red'}">This div is special</div>
    • Note: if more than one word property is used, e.g. btn-primary, we wrap it using quotations.

ngClass vs ngStyle

  1. If the element requires multiple styles within a class, using ngClass expressions is a good idea. Only for situations where we have a dynamically calculated embedded style should we use ngStyle.
  2. ngStyle is used to interpolate JavaScript object into style attribute, not css class. Following directive will be translated to style="color:red" nStyle="{color: 'red'}"
    • And ngClass directive translates your object into class attribute. Following will be translated to class="deleted" when isDeleted variable is true. ngClass="{'deleted': isDeleted}"
  3. Must read: 1 and 2

Custom Directives

We can create custom attribute directives and custom structural directives using a @Directive decorator. This section describes

Custom Attribute Directives

We can create custom attribute directives and custom structural directives using a @Directive decorator. Example: I want to build a directive which simply highlights an element. Now to configure a directive follow these steps:

  • The one thing our directive absolutely needs is a selector because remember, we do place directives in our template to attach them to elements, so we need to have some way to give Angular that instruction and that is the selector. Here that should also be a unique selector e.g. appBasicHighlight. Now I want to have this attribute style, so I'm going to wrap this in square brackets e.g. [appBasicHighlight] without square brackets to an element.

  • Now we need to get access to the element the directive sits on and Angular gives us this access. We can inject the element the directive sits on into this directive. We do inject by adding the constructor. We list a couple of arguments we want to get whenever an instance of this class here is created.

  • We need is a reference to the element the directive was placed on. So we have an variable of the type ElementRef.

  • So what we're doing here is we're getting access to the element the directive was placed on, getting access to that exact element and then we're overriding the style of this element.

  • Importantly, we don't use square brackets because as the directive name is just a selector we set up here and the square brackets here are not part of that name, it's part of this selector style telling Angular please select it as an attribute on an element and that's just how we add it here, like an attribute of the paragraph.

import { Directive, OnInit, ElementRef } from '@angular/core';
@Directive({
    selector: '[appBasicHighlight]'
})
export class BasicHighlightDirective implements OnInit {
    constructor(private elementRef: ElementRef) { }
    ngOnInit(): void {
        this.elementRef.nativeElement.style.backgroundColor = 'black';
        this.elementRef.nativeElement.style.color = 'white';
        this.elementRef.nativeElement.style.textAlign = 'center';
    }
}
<p appBasicHighlight>Directive Test</p>

Alternate approach using Renderer2

  • To render your templates without a DOM (i.e. environments where you might not have access to the DOM) and then these properties might not be available. It could do this when using service workers, so basically some advanced use cases but nonetheless, it's not a good practice to directly access your elements. To avoid this you can inject dependency via Renderer2
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core';
@Directive({
  selector: '[appBatterHighlight]'
})
export class BatterHighlightDirective implements OnInit {
  constructor(private renderer: Renderer2, private elementRef: ElementRef) { }
  ngOnInit(): void {
    this.renderer.setStyle(this.elementRef.nativeElement, 'background-color', 'black');
    this.renderer.setStyle(this.elementRef.nativeElement, 'color', 'white');
    this.renderer.setStyle(this.elementRef.nativeElement, 'text-align', 'center');
  }
}
<p appBatterHighlight>Rendered2 Directive Test</p>

Host Listener

  • @HostListener() is the Decorator that declares a DOM event to listen for, and provides a handler method to run when that event occurs.

  • Now this can be triggered whenever some event occurs and that event is specified here as an argument, as a string. @HostListener(eventName) here takes the argument name as an input and that would be mouseenter and mouseleave events

  • You can also listen to custom events here and retrieve that data e.g. (event: Event)

@HostListener('mouseenter') mouseover(event: Event) {
    this.renderer.setStyle(this.elementRef.nativeElement, 'background-color', 'black');
    this.renderer.setStyle(this.elementRef.nativeElement, 'color', 'white');
    this.renderer.setStyle(this.elementRef.nativeElement, 'text-align', 'center');
  }
  @HostListener('mouseleave') mouseleave(event: Event) {
    this.renderer.setStyle(this.elementRef.nativeElement, 'background-color', 'inherit');
    this.renderer.setStyle(this.elementRef.nativeElement, 'color', 'inherit');
    this.renderer.setStyle(this.elementRef.nativeElement, 'text-align', 'inherit');
  }

Host Binder

  • The @HostBinding() function decorator allows you to set the properties of the host element from the directive class. Let's say you want to change the style properties such as height, width, color, margin, border, etc., or any other internal properties of the host element in the directive class. Here, you'd need to use the @HostBinding() decorator function to access these properties on the host element and assign a value to it in directive class.

  • @HostBinding() also allows us to listen to events without using the renderer. There is nothing wrong with using the renderer but we get another way of simply changing the behavior in the directive.

  • In @HostBinding(property), we can pass a string defining to which property of the hosting element we want to bind. Now properties of the hosting element, that is simply what we also access here in the directive e.g. style would be such a property

  • Therefore we can simply say style and camel case backgroundColor and that's all. Camel case is important here because we're accessing the DOM property which doesn't know dashes.

  @HostBinding('style.backgroundColor') backgroundColor: string = 'inherit';
  @HostBinding('style.color') color: string = 'inherit';
  @HostBinding('style.textAlign') textAlign: string = 'inherit';

  @HostListener('mouseenter') mouseover(event: Event) {
    this.backgroundColor = 'black';
    this.color = 'white';
    this.textAlign = 'center';
  }
  @HostListener('mouseleave') mouseleave(event: Event) {
    this.backgroundColor = 'inherit';
    this.color = 'inherit';
    this.textAlign = 'inherit';
  }
  • You can also bind a property when it is true. Let's say you want to add a class 'open' when user clicks the button and then remove this newly added class when user clicks on button second time i.e. you want to toggle 'open' class
    @HostBinding('class.open') isOpen: boolean = false;

    @HostListener('click') toggleOpen() {
        this.isOpen = !this.isOpen;
    }

Dynamic Property Binding

  • You can make directives listening to user-input by property binding, e.g.
<p appBatterHighlight [defaultHighlightColor]="'white'" 
                      [defaultColor]="'black'" 
                      [defaultTextAlign]="'left'"
                      [onHoverHighlightColor]="'black'" 
                      [onHoverColor]="'white'" 
                      [onHoverTextAlign]="'center'">Rendered2 Directive Test</p>
  • Take note of input "''", directives need string value, so you need to pass single-quotations within double-quotations. And in corresponding TS file, make following changes
  @Input() defaultColor: string = 'inherit';
  @Input() defaultHighlightColor: string = 'inherit';
  @Input() defaultTextAlign: string = 'inherit';
  @Input() onHoverColor: string = 'white';
  @Input() onHoverHighlightColor: string = 'black';
  @Input() onHoverTextAlign: string = 'center';

  @HostBinding('style.backgroundColor') backgroundColor: string;
  @HostBinding('style.color') color: string;
  @HostBinding('style.textAlign') textAlign: string;

  ngOnInit(): void {
    this.backgroundColor = this.defaultHighlightColor;
    this.color = this.defaultColor;
    this.textAlign = this.defaultTextAlign;
  }

  @HostListener('mouseenter') mouseover(event: Event) {
    this.backgroundColor = this.onHoverHighlightColor;
    this.color = this.onHoverColor;
    this.textAlign = this.onHoverTextAlign;
  }
  @HostListener('mouseleave') mouseleave(event: Event) {
    this.backgroundColor = this.defaultHighlightColor;
    this.color = this.defaultColor;
    this.textAlign = this.defaultTextAlign;
  }
  • Make sure default initialization is is in ngOnInit() else default styling will not set

Custom Structural Directive

  • l'll create a directive named unless, it basically performs functions that are opposite of the ngIf directive! i.e. this directive here will attach something only if the condition is false, ngIf does it if the condition is true.

  • Now here, I need to get the condition as an input, so I'll add @Input() and then here, I want to bind to a property named appUnless, which kind of simply is the condition we get but whenever this condition changes, so whenever some input parameter here changes, I want to execute a method and therefore, I can implement a setter with the set keyword.

    @Input() set appUnless(condition: boolean) {}
  • This now turns this into a method, though technically and that's important to understand, this still is a property, it's just a setter of the property which is a method which gets executed whenever the property changes and it of course changes whenever it changes outside of this directive, so whenever the condition we pass changes or some parameter of this condition.

  • Unless therefore needs to receive the value, the property we would normally get as an input and we know that this will be a boolean because it will be our condition in the end, so we could also name this condition.

  • Then we can check if the condition is not true, which is the case in which I want to display something because unless is the opposite of ngIf and if the condition is true, well then I want to display nothing. So that is how we get the condition.

    @Input() set appUnless(condition: boolean) {
      if (!condition) {
        //do something
      } else {
        //do nothing
      }
    }   
  • Keep in mind that our unless directive here in the end will sit on such an ng-template component because that is what it gets transformed to by Angular if we use the * {reference}. So we can get access to this template and we also need to get access to the place in the document where we want to render it, both can be injected.

  • The template can be injected by adding template reference which is of type TemplateRef, so just like ElementRef gave us access to the element the directive was on, TemplateRef does the same for a template and this is a generic type, you can simply pass any here and we need to import TemplateRef from @angular/core.

  • The second information piece we need is the ViewContainer, so where should we render it? The template is the what, now the question is where.

  • I'll create a variable vcRef for view container reference and the type is ViewContainer which is also imported from @angular/core. That marks the place where we placed this directive in the document. Angular marks this place and you can see this if you inspect it in the developer tools actually.

    constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef) { }
  • So with these two tools available, we can use the vcRef whenever the condition changes, to call the createEmbeddedView method which creates a view in this ViewContainer and the view simply is our TemplateRef when the condition is false.

    this.vcRef.createEmbeddedView(this.templateRef);
  • Well and if the condition is true in this case, then we will simply call the clear method to remove everything from this place in the DOM.

    this.vcRef.clear();
  • Our final .ts file that holds directive logic will be like

    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    @Directive({
      selector: '[appUnless]'
    })
    export class UnlessDirective {
       @Input() set appUnless(condition: boolean) {
          if (!condition) {
            this.vcRef.createEmbeddedView(this.templateRef);
          } else {
            this.vcRef.clear();
          }
       }
       constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef) { }
    }
  • We can use this directive as

    <div *appUnless="onlyOdd">
    ...
    </div>
  • Now keep in mind, the * automatically transforms this in this <ng-template> syntax where we then try to property bind to the directive name which is appUnless. So we have to make sure that our property here shares the name of the directive, appUnless, exactly the same, the same as the selector.


Miscellaneous Section

Understanding Template Reference Variable

  • A template reference variable is often a reference to a DOM element within a template. It can also refer to a directive (which contains a component), an element, TemplateRef, or a web component

  • Use the hash symbol # to declare a reference variable. The following reference variable, #phone, declares a phone variable on an <input> element. <input #phone placeholder="phone number" />

  • You can refer to a template reference variable anywhere in the component's template. Here, a <button> further down the template refers to the phone variable. <button (click)="callPhone(phone.value)">Call</button>

  • More reading

Understanding <ng-template>

  • The ng-template directive represents an Angular template: this means that the content of this tag will contain part of a template, that can be then be composed together with other templates in order to form the final component template. E.g.
@Component({
  selector: 'app-root',
  template: `      
   <ng-template>
       <button class="tab-button" (click)="login()">{{loginText}}</button>
       <button class="tab-button" (click)="signUp()">{{signUpText}}</button>
   </ng-template>
`})
  • If you try the example above, you might be surprised to find out that this example does not render anything to the screen! This is normal and it's the expected behavior. This is because with the ng-template tag we are simply defining a template, but we are not using it yet.

  • ng-template is used with structural directives: ngIf, ngFor and ngSwitch

<div class="lessons-list" *ngIf="lessons else loading">
  ... 
</div>

<ng-template #loading>
    <div>Loading...</div>
</ng-template>
  • This is a very common use of the ngIf/else functionality: we display an alternative loading template while waiting for the data to arrive from the backend. As we can see, the else clause is pointing to a template, which has the name loading. The name was assigned to it via a template reference, using the #loading syntax.

  • More reading

Clone this wiki locally