Tutorial

How To Create Reusable Components with NgTemplateOutlet in Angular

Updated on August 21, 2021
author

Mark P. Kennedy

How To Create Reusable Components with NgTemplateOutlet in 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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Mark P. Kennedy

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
1 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Nice article. I was wondering if using the ideas from it I could accomplish something like this. I am using ng-template to delay component instantiation until data is available. Works just fine. At some point that component will get destroyed. I think I can cache it just before it happens. But then the component will be created again in the same template. At this point is it possible to reuse the cached object instead creating it again?

Thanks

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.