Getting Started with Custom Structural Directives in Angular
Tags: angular, typescript, webdev, directive
Published: 2021-11-23 09:02:10

## Introduction

Angular comes with many built-in directives. Some of them (eg. NgIf, NgModel or FormControlDirective) are used daily by Angular developers. Those directives can be split into 2 categories:

- Attribute directives

  They can be used to modify the appearance of behavior of Angular components and DOM elements. For example:

  - [RouterLink](https://angular.io/api/router/RouterLink)
  - [NgModel](https://angular.io/api/forms/NgModel)
  - [FormControlDirective](https://angular.io/api/forms/FormControlDirective)

- Structural directives

  They can be used to manipulate the HTML structure in the DOM. Using them, we can change the structure of part of the DOM that they control. For example:

  - [NgIf](https://angular.io/api/common/NgIf)
  - [NgForOf](https://angular.io/api/common/NgForOf)
  - [NgSwitch](https://angular.io/api/common/NgSwitch)

In this article, I will focus on the latter.

## Creating a custom structural directive

As I've mentioned above, there are a couple of built-in structural directives in Angular. However, we might come across a case that the ones provided with the framework don't solve. This is where a custom structural directive might help us resolve the issue. But how do we write one?

---

All the code examples in this article use the Angular CLI or Nx CLI generated project as a starting point. You can generate a project using the following command, or use [Stackblitz starter project](https://stackblitz.com/github/nrwl/stackblitz-nx-angular?preset=node).

```shell
npx create-nx-workspace --preset=angular --prefix=csd --appName=custom-structural-directive

# or
# ng new custom-structural-directive --prefix=csd
```

---

### NgIf directive clone

Let's learn the basic concepts by reimplementing the basic features of the `NgIf` directive. We will call it `CsdIf` (CSR prefix stands for Custom Structural Directive :))

The structural directive is actually just a regular directive (with some additional syntactic sugars provided by Angular). So we can start with creating a module and empty directive using AngularCLI:

```shell
ng generate module if
ng generate directive if/if --module if

# or shorthand
# ng g m if
# ng g d if/if --module if
```

our new directive should look like this:

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

@Directive({
  selector: '[csdIf]',
})
export class IfDirective {
  constructor() {}
}
```

Let's implement the basic functionality of displaying the content if passed value is `true`.

```html
<h2 *csdIf="true">My visible conditional header</h2>
<h2 *csdIf="false">My hidden conditional header</h2>
```

To achieve that, we need a couple of elements:

- an input that will determine whether to show or hide the content (`@Input`)
- a reference to the template that we want to conditionally display (`TemplateRef`)
- a container that will provide us with access to Angular's view (`ViewContainerRef`)

The input can be just a regular class property with Angular's `@Input` decorator. The important thing is to use a proper naming convention. For it to work as it does in the example code shown above, we need to name the property the same as the attribute's selector:

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

@Directive({
  selector: '[csdIf]',
})
export class IfDirective {
  @Input() csdIf: boolean = false;

  constructor() {}
}
```

Now our directive has the information whether to display the content or not but we need to also gain access to the TemplateRef and ViewContainerRef instances. We can do that by injecting them via a constructor:

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

@Directive({
  selector: '[csdIf]',
})
export class IfDirective {
  @Input() csdIf: boolean = false;

  constructor(
    private templateRef: TemplateRef<unknown>,
    private vcr: ViewContainerRef
  ) {}
}
```

Now we have all the necessary tools and information to display or hide the content. We can use `ViewContainerRef`'s `createEmbeddedView` method to display and `clear` method to remove the content.
**Important note:** To make sure the `csdIf` property is assigned already, we need to use `ngOnInit` lifecycle hook.

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

@Directive({
  selector: '[csdIf]',
})
export class IfDirective implements OnInit {
  @Input() csdIf: boolean = false;

  constructor(
    private templateRef: TemplateRef<unknown>,
    private vcr: ViewContainerRef
  ) {}

  ngOnInit(): void {
    if (this.csdIf) {
      this.vcr.createEmbeddedView(this.templateRef);
    } else {
      this.vcr.clear();
    }
  }
}
```

With this implementation, the following example already works as expected.

```html
<h2 *csdIf="true">My visible conditional header</h2>
<h2 *csdIf="false">My hidden conditional header</h2>
```

![csdIf - first result](//images.contentful.com/zojzzdop0fzx/5T3mjVVkLWsK8oIWmgiNYl/1222be8721cd592db9861ff455075fb5/Screenshot_2021-10-22_at_13.42.39.png)

There is still a problem with this implementation. Let's try to use the following example:

```html
<input id="showInput" type="checkbox" [(ngModel)]="showInput" />
<label for="showInput">Show conditional header</label>
<h2 *csdIf="showInput">My conditional header</h2>
```

The "My conditional header" is displayed correctly when the page renders but as soon as we uncheck the `showInput`, our header doesn't disappear as we would expect. This is because we only check the `csdIf` input value inside of `ngOnInit`, but we do not react to the input's changes. To resolve this, we can either use `ngOnChanges` lifecycle hook or modify the `csdIf` to be a setter rather than just a property. I will show you the later solution but implementing it using `ngOnChanges` should be very similar.

As a first step, let's modify the `csdIf` to be a setter, and store its value in a private property `show`.

```ts
@Directive({
  selector: '[csdIf]',
})
export class IfDirective implements OnInit {
  private show = false;
  @Input() set csdIf(show: boolean) {
    this.show = show;
  }

  /* constructor */

  ngOnInit(): void {
    if (this.show) {
      this.vcr.createEmbeddedView(this.templateRef);
    } else {
      this.vcr.clear();
    }
  }
}
```

Secondly, when the new `csdIf` value is set, we need to perform the same logic as we do in `ngOnInit`. We need to make sure though that we don't render the template twice so we can clear the view first in all cases.

```ts
@Directive({
  selector: '[csdIf]',
})
export class IfDirective implements OnInit {
  private show = false;
  @Input() set csdIf(show: boolean) {
    this.show = show;
    this.vcr.clear();
    if (this.show) {
      this.vcr.createEmbeddedView(this.templateRef);
    }
  }

  /* constructor */

  ngOnInit(): void {
    this.vcr.clear();
    if (this.show) {
      this.vcr.createEmbeddedView(this.templateRef);
    }
  }
}
```

As a final step, let's refactor to remove the code duplication by extracting the common logic into a method.

```ts
@Directive({
  selector: '[csdIf]',
})
export class IfDirective implements OnInit {
  private show = false;
  @Input() set csdIf(show: boolean) {
    this.show = show;
    this.displayTemplate();
  }

  /* constructor */

  ngOnInit(): void {
    this.displayTemplate();
  }

  private displayTemplate() {
    this.vcr.clear();
    if (this.show) {
      this.vcr.createEmbeddedView(this.templateRef);
    }
  }
}
```

Now, our second example works as expected:

```html
<input id="showInput" type="checkbox" [(ngModel)]="showInput" />
<label for="showInput">Show conditional header</label>
<h2 *csdIf="showInput">My conditional header</h2>
```

### Handling additional parameters - else template

The `CsdIf` directive shows and hides the content based on the boolean input correctly. But the original `NgIf` directive allows for specifying an alternative template via the "else" property as well. How do we achieve this behavior in our custom directive? This is where understanding the "syntactic sugar" that stands behind the structural directives is crucial. The following `NgIf` syntax:

```html
<h2 *ngIf="show; else alternativeTemplate">My conditional header</h2>
<ng-template #alternativeTemplate>
  <h2>My alternative header</h2>
</ng-template>
```

is actually equivalent to the following syntax:

```html
<ng-template [ngIf]="show" [ngIfElse]="alternativeTemplate">
  <h2>My conditional header</h2>
</ng-template>
<ng-template #alternativeTemplate>
  <h2>My alternative header</h2>
</ng-template>
```

This means that the `else` property is actually becoming `ngIfElse` input parameter. In general, we can construct the property name by concatenating the attribute following `*` and the capitalized property name (eg. `"ngIf"` + `"Else"` = `"ngIfElse""`). In case of our custom directive it will become `"csdIf"` + `"Else"` = `"csdIfElse`

```html
<h2 *csdIf="show; else alternativeTemplate">My conditional header</h2>
<ng-template #alternativeTemplate>
  <h2>My alternative header</h2>
</ng-template>
```

is equivalent to

```html
<ng-template [csdIf]="show" [csdIfElse]="alternativeTemplate">
  <h2>My conditional header</h2>
</ng-template>
<ng-template #alternativeTemplate>
  <h2>My alternative header</h2>
</ng-template>
```

By analyzing the "unwrapped" syntax we can notice the the reference to an alternative template is passed via the `csdIfElse` property. Let's add and handle that property in the custom directive implementation:

```ts
@Directive({
  selector: '[csdIf]',
})
export class IfDirective implements OnInit {
  private show = false;

  @Input() set csdIf(show: boolean) {
    this.show = show;
    this.displayTemplate();
  }

  @Input() csdIfElse?: TemplateRef<unknown>;

  /* constructor */

  ngOnInit(): void {
    this.displayTemplate();
  }

  private displayTemplate() {
    this.vcr.clear();
    if (this.show) {
      this.vcr.createEmbeddedView(this.templateRef);
    } else if (this.csdIfElse) {
      this.vcr.createEmbeddedView(this.csdIfElse);
    }
  }
}
```

This addition makes our directive much more useful, and allows for displaying content for cases when the condition is true or false.

If something is not clear, or you want to play with the example directive please visit the [example on Stackblitz](https://stackblitz.com/github/ktrz/custom-structural-directive/tree/getting-started-solution?file=apps%2Fcustom-structural-directive%2Fsrc%2Fapp%2Fif%2Fif.directive.ts&preset=node).

## Real life example

The above example is very simple, but it gives you tools to create your own custom directive when you need it. If you want to have a look at some real-life custom directive example that we've found useful at This Dot Labs, I suggest checking out our [route-config](https://www.npmjs.com/package/@this-dot/route-config) open source library. You can read more about it in one of our articles:

- [Introducing @this-dot/route-config](https://www.thisdot.co/blog/introducing-our-first-open-source-library-this-dot-route-config)
- [What's new in @this-dot@route-config v1.2](https://www.thisdot.co/blog/whats-new-in-this-dot-route-config-v1-2)

## Summary

In this article, we've learnt how to write a simple custom structural directive that handles additional inputs. We've covered the syntactic sugar that stands behind the structural directive, and how it translates into directive's inputs. In the second part, I will show you how to add some additional functionalities to the custom structural directive and present ways to improve type checking experience for the custom directive's templates.

In case you have any questions, you can always tweet or DM me at [@ktrz](https://twitter.com/ktrz__). I'm always happy to help!

---

_This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies._

_We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more._