Tutorial

How To Create Reusable Components with NgTemplateOutlet in Angular

Angular

Introduction

The single responsibility principle is the idea that pieces of your application should have one purpose. Following this principle makes your Angular app easier to test and develop.

In Angular, using NgTemplateOutlet instead of creating specific components allows for components to be easily modified for various use cases without having to modify the component itself!

In this article, you will take an existing component and rewrite it to use NgTemplateOutlet.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v16.6.2, npm v7.20.6, and @angular/core v12.2.0.

Step 1 – Constructing CardOrListViewComponent

Consider CardOrListViewComponent which displays items in a 'card' or a 'list' format depending on its mode.

It consists of a card-or-list-view.component.ts file:

card-or-list-view.component.ts
import {
  Component,
  Input
} from '@angular/core';

@Component({
  selector: 'card-or-list-view',
  templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {

  @Input() items: {
    header: string,
    content: string
  }[] = [];

  @Input() mode: string = 'card';

}

And a card-or-list-view.component.html template:

card-or-list-view.component.html
<ng-container [ngSwitch]="mode">
  <ng-container *ngSwitchCase="'card'">
    <div *ngFor="let item of items">
      <h1>{{item.header}}</h1>
      <p>{{item.content}}</p>
    </div>
  </ng-container>
  <ul *ngSwitchCase="'list'">
    <li *ngFor="let item of items">
      {{item.header}}: {{item.content}}
    </li>
  </ul>
</ng-container>

Here is an example of the usage of this component:

usage.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <card-or-list-view
        [items]="items"
        [mode]="mode">
    </card-or-list-view>
`
})
export class UsageExample {
  mode = 'list';
  items = [
    {
      header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
      content: 'The single responsibility principle...'
    } // ... more items
  ];
}

This component does not have a single responsibility and isn’t very flexible. It needs to keep track of its mode and know how to display items in both card and list view. And it can only display items with a header and content.

Let’s change that by breaking the component into separate views using templates.

Step 2 – Understanding ng-template and NgTemplateOutlet

In order to allow the CardOrListViewComponent to display any kind of items we need to be able to tell it how to display them. We can achieve this by giving it a template that it can use to stamp out the items.

The templates will be TemplateRefs using <ng-template> and the stamps will be EmbeddedViewRefs created from the TemplateRefs. EmbeddedViewRefs represent views in Angular with their own context and are the smallest essential building block.

Angular provides a way to use this concept of stamping out views from templates with NgTemplateOutlet.

NgTemplateOutlet is a directive that takes a TemplateRef and context and stamps out an EmbeddedViewRef with the provided context. The context is accessed on the template via let-{{templateVariableName}}="contextProperty" attributes to create a variable the template can use. If a context property name is not provided, it will choose the $implicit property.

Here is an example:

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

@Component({
  template: `
    <ng-container *ngTemplateOutlet="templateRef; context: exampleContext"></ng-container>
    <ng-template #templateRef let-default let-other="aContextProperty">
      <div>
        $implicit = '{{default}}'
        aContextProperty = '{{other}}'
      </div>
    </ng-template>
`
})
export class NgTemplateOutletExample {
  exampleContext = {
    $implicit: 'default context property when none specified',
    aContextProperty: 'a context property'
  };
}

Here is the output from the example:

<div>
  $implicit = 'default context property when none specified'
  aContextProperty = 'a context property'
</div>

The default and other variables are provided by the let-default and let-other="aContextProperty" props.

Step 3 – Refactoring CardOrListViewComponent

To provide flexibility to the CardOrListViewComponent and allow it to display any type of items, we will create two structural directives to read in as templates. These templates will be the card and list item.

Here is card-item.directive.ts:

card-item.directive.ts
import { Directive } from '@angular/core';

@Directive({
  selector: '[cardItem]'
})
export class CardItemDirective {

  constructor() { }

}

And here is list-item.directive.ts:

list-item.directive.ts
import { Directive } from '@angular/core';

@Directive({
  selector: '[listItem]'
})
export class ListItemDirective {

  constructor() { }

}

CardOrListViewComponent will import CardItemDirective and ListItemDirective:

card-or-list-view.component.ts
import {
  Component,
  ContentChild,
  Input,
  TemplateRef 
} from '@angular/core';
import { CardItemDirective } from './card-item.directive';
import { ListItemDirective } from './list-item.directive';

@Component({
  selector: 'card-or-list-view',
  templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {

  @Input() items: {
    header: string,
    content: string
  }[] = [];

  @Input() mode: string = 'card';

  @ContentChild(CardItemDirective, {read: TemplateRef}) cardItemTemplate: any;
  @ContentChild(ListItemDirective, {read: TemplateRef}) listItemTemplate: any;

}

This code will read in our structural directives as TemplateRefs.

card-or-list-view.component.html
<ng-container [ngSwitch]="mode">
  <ng-container *ngSwitchCase="'card'">
    <ng-container *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="cardItemTemplate"></ng-container>
    </ng-container>
  </ng-container>
  <ul *ngSwitchCase="'list'">
    <li *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="listItemTemplate"></ng-container>
    </li>
  </ul>
</ng-container>

Here is an example of the usage of this component:

usage.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <card-or-list-view
        [items]="items"
        [mode]="mode">
      <div *cardItem>
        Static Card Template
      </div>
      <li *listItem>
        Static List Template
      </li>
    </card-or-list-view>
`
})
export class UsageExample {
  mode = 'list';
  items = [
    {
      header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
      content: 'The single responsibility principle...'
    } // ... more items
  ];
}

With these changes, the CardOrListViewComponent can now display any type of item in the card or list form based on the template provided. Currently, the templates are static.

The last thing we need to do is allow the templates to be dynamic by giving them a context:

card-or-list-view.component.html
<ng-container [ngSwitch]="mode">
  <ng-container *ngSwitchCase="'card'">
    <ng-container *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="cardItemTemplate; context: {$implicit: item}"></ng-container>
    </ng-container>
  </ng-container>
  <ul *ngSwitchCase="'list'">
    <li *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="listItemTemplate; context: {$implicit: item}"></ng-container>
    </li>
  </ul>
</ng-container>

Here is an example of the usage of this component:

usage.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <card-or-list-view
        [items]="items"
        [mode]="mode">
      <div *cardItem="let item">
        <h1>{{item.header}}</h1>
        <p>{{item.content}}</p>
      </div>
      <li *listItem="let item">
        {{item.header}}: {{item.content}}
      </li>
    </card-or-list-view>
`
})
export class UsageExample {
  mode = 'list';
  items = [
    {
      header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
      content: 'The single responsibility principle...'
    } // ... more items
  ];
}

The interesting thing to note is that we use the asterisk prefix and microsyntax for syntactical sugar. It is the same as:

<ng-template cardItem let-item>
  <div>
    <h1>{{item.header}}</h1>
    <p>{{item.content}}</p>
  </div>
</ng-template>

And that’s it! We have the original functionality, but now we can display whatever we want by modifying the templates and the CardOrListViewComponent has less responsibility. We can add more to the item context like first or last similar to ngFor or display completely different types of items.

Conclusion

In this article, you took an existing component and rewrote it to use NgTemplateOutlet.

If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.

Creative Commons License