Creating Reusable Components with NgTemplateOutlet in Angular

Mark P. Kennedy

The single responsibility principle is the idea that pieces of your app should have one purpose. Following this principle makes your Angular app easier to test and develop. In this post we will be breaking a CardOrListViewComponent into 🐊 bite-sized pieces with the help of NgTemplateOutlet. 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!

The CardOrListViewComponent displays items in a card or a list format depending on its mode:

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>

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: 'card' | 'list' = 'card';

}

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 (it needs to keep track of its mode and know how to display items in both card and list view) and isn’t very flexible (it can only display items with a header and content). Let’s change that by breaking the component into separate views using templates.

Templates and Stamps

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 do 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 an easy 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. The following 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'
  };
}

Will output:

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

Making the Templates

To provide flexibility to the CardOrViewComponent (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:

card-item.directive.ts

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

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

list-item.directive.ts

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

@Directive({
  selector: '[listItem]'
})
export class 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: any[] = [];

  @Input() mode: 'card' | 'list' = 'card';

  // Read in our structural directives as TemplateRefs
  @ContentChild(CardItemDirective, {read: TemplateRef}) cardItemTemplate;
  @ContentChild(ListItemDirective, {read: TemplateRef}) listItemTemplate;

}

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>

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>

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.

Embace the power of NgTemplateOutlet

✖ Clear

🕵 Search Results

🔎 Searching...