Skip to content

Using structural directives in Angular

Structural Directive Cover

In the last article, we talked about attribute directives. In a natural continuation of the theme, we’ll now mention structural directives, too. So what are they, how do they differ from attribute directives, and what are we using them for?

Structural directives

In short, structural directives change the layout of the DOM by appending or removing DOM elements. The most commonly used structural directive is *ngIf which conditionally shows or removes views from the template. There is also *ngFor which renders lists of items, and *ngSwitch, used for switching between alternate views.

<div *ngIf="clicks === 5">Only show if the variable clicks is equal to 5!</div>
<tr *ngFor="let invoice of invoices">
  <td>{{ invoice.id }}</td>
</tr>
<ul [ngSwitch]="fruit">
  <li *ngSwitchCase="'Apples'">Apples</li>
  <li *ngSwitchCase="'Bananas'">Bananas</li>
  <li *ngSwitchCase="'Cherries'">Cherries</li>
  <li *ngSwitchDefault>Oranges</li>
</ul>

Above we created an *ngIf check which will render the context of the div element only if the variable clicks is equal to 5. There is another interesting, but not well-known *ngIf functionality. We can (optionally) provide the alternative template by writing else.

<div *ngIf="clicks === 5; else elseBlock">Only show if the variable clicks is equal to 5!</div>
<ng-template #elseBlock>Show otherwise.</ng-template>

The *ngFor or *ngForOf directive renders a template for every element in an iterable collection. We can additionally use exported values in an *ngFor directive such as index, first and count. Additionally, we can provide a custom trackBy method to optimize the rendering of the directive.

<li
  *ngFor="
    let invoice of invoices;
    index as i;
    count as count;
    trackBy: trackByFn
  "
>
  {{ invoice.id }} <span>Total: {{ count }} elements</span>
</li>

The *ngSwitch the directive will specify a condition to match, that will render all the elements that match that condition. In the example we matched the switch expression to a variable named fruit in our component. Depending on the value of the variable we would match the condition (or multiple) that are truth. Otherwise, we would print out the default value:

<ul [ngSwitch]="fruit">
  <li *ngSwitchCase="'Apples'">Apples</li>
  <li *ngSwitchCase="'Bananas'">Bananas</li>
  <li *ngSwitchCase="'Cherries'">Cherries</li>
  <li *ngSwitchDefault>Oranges</li>
</ul>

Custom directive

There is a pretty cool specialized statement in Ruby (and Perl and some other languages), known as unless, which does the exact opposite of what if does, namely, it executes the statement only if the statement is false. So let us do something similar in Angular. We’ll use a slightly modified project from the article on attribute directives.

First, we’ll generate the unless directive.

import { Directive } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {

  constructor() { }
}

Next thing, let us create a component variable called shouldHide, and assign it to the template as well.

<div class="wrapper">
  <div class="button-wrappers">
    <button (click)="show()">Show</button>
    <button (click)="hide()">Hide</button>
  </div>

  <p *appUnless="shouldHide">
    ...
  </p>
</div>
export class TextCardComponent implements OnInit {
  shouldHide: boolean = false;

  constructor() {}

  ngOnInit() {}

  show(): void {
    this.shouldHide = false;
  }

  hide(): void {
    this.shouldHide = true;
  }
}

The next thing to do is set up the directive itself. In order to track all the changes to the input variable of shouldHide, we’ve created a setter (another alternative would have been to create the onChanges lifecycle hook).

import { Directive, Input } from '@angular/core';

@Directive({
  selector: '[appUnless]',
})
export class UnlessDirective {
  @Input() set appUnless(shouldHide: boolean) {
    console.log(shouldHide);
  }

  constructor() {}
}

Next, we’ll write the implementation. We’ll use the TemplateRef and ViewContainerRef classes, in order to instantiate embedded views based on a template. For more information, you can refer to this post on this blog.

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]',
})
export class UnlessDirective {
  constructor(
    private templateRef: TemplateRef<HTMLElement>,
    private vcRef: ViewContainerRef
  ) {}
  @Input() set appUnless(shouldHide: boolean) {
    if (shouldHide) {
      this.vcRef.clear();
    } else {
      this.vcRef.createEmbeddedView(this.templateRef);
    }
  }
}

Now this works exactly like an *ngIf would. The difference is that we inversed the logic in the directive itself, so we’re actually triggering the embedded view creating if the variable provided is false.

Structural Directive finished example
Structural Directive finished example

The StackBlitz for this project can be found below: