Using NgTemplateOutlet

See the docs here: https://angular.io/api/common/NgTemplateOutlet

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
  ];
}

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. The templates will be TemplateRefs using <ng-template> and the stamps will be EmbeddedViewRefs created from the TemplateRefsEmbeddedViewRefs 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>

Published by anthonykuong

Anthony is a versatile Software professional with around 10 years of experience. He is a Full Stack developer experienced with clients in the Financial, Health and Supply Chain industries. He is experienced with MVC frameworks ( Spring Boot) , SPA frameworks ( Angular , VueJS), and also supports automated build deployments and packaging for development, qa, and production servers.. He has delivered rich user experience using Modern web technologies and techniques such are HTML5, CSS3, ECMAScript 6 (ES6)/ ECMAScript 2015, CSS pre-processors (SASS, Less), JavaScript build tools (Grunt, Gulp) , various UI Frameworks including AngularJS , Knockout JS , and CSS Frameworks including Bootstrap, and Foundation. He is adaptable to new technologies and frameworks. He is a rigorous, quality-conscious contributor with solid analytical skills. I can also be found on youtube - Youtube Channel: https://www.youtube.com/user/akuong/

Leave a comment