What is ngTemplateOutlet in Angular? (And when to use it)


ngTemplateOutlet is more useful than you might realize

What is ngTemplateOutlet in Angular?

Many just don't get the use case for ngTemplateOutlet. And can't seem to understand why use it in your own code.

And it gets worse...

If you got to the official documentation for ngTemplateOutlet you'll see a description like this.

Inserts an embedded view from a prepared TemplateRef.

You can attach a context object to the EmbeddedViewRef by setting [ngTemplateOutletContext]. [ngTemplateOutletContext] should be an object, the object's keys will be available for binding by the local template let declarations.

That just makes it more confusing huh?

confusing

So today we're going to build a customizable table component and this will help you see the reason for ngTemplateOutlet.

What is ngTemplate and ngTemplateOutlet?

So the main idea is that we can use ngTemplate to create a chunk of a template that won't actually be rendered to the DOM.

And instead we can dynamically control when to display this template. This was very common before the new Angular control flow.

A common example of this would be a loading template while waiting for something else to finish.

<p *ngIf="false; else myTemplate">Done</p>
<ng-template #myTemplate>
    Done
</ng-template>

And then we could use this template over and over again with the help of ngTemplateOutlet.

<p *ngIf="false; else myTemplate">Done</p>

<ng-template #myTemplate>
    Done
</ng-template>

<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>

And we could also configure the Angular template to accept variables. 😎 😎 😎

<ng-template #myTemplate let-greeting="greeting" let-message="message">
  <p>{{greeting}} {{message}}</p>
</ng-template>

<ng-container *ngTemplateOutlet="myTemplate; context: {greeting: 'Hi', message: 'Angular'}"></ng-container>

You're probably noticing that this is kind of like what a component does. You can create a child Angular component by passing in different inputs to control what it displays.

So why not just use a component instead of defining all these weird template outlets?

Well, templates can give us more power, if we use them well.

Follow along buddy!

To demonstrate the power of ngTemplateOutlet we're going to create a dynamic table component.

1. Creating the template

We can use the Angular CLI to create our dynamic table.

ng generate component table

And then our demo code.

import { CommonModule } from '@angular/common';
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-table',
  standalone: true,
  imports: [CommonModule],
  template: `
    <table>
      <thead>
        <tr>
          <ng-container
            *ngTemplateOutlet="
              headers || defaultHeaderTemplate;
              context: { $implicit: data }
            "
          ></ng-container>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let row of data">
          <ng-container
            *ngTemplateOutlet="
              rows || defaultRowTemplate;
              context: { $implicit: row }
            "
          ></ng-container>
        </tr>
      </tbody>
    </table>

    <ng-template #defaultHeaderTemplate let-data>
      <th *ngFor="let header of data[0] | keyvalue">{{ header.key }}</th>
    </ng-template>

    <ng-template #defaultRowTemplate let-row>
      <td *ngFor="let row of row | keyvalue">{{ row.value }}</td>
    </ng-template>
  `,
  styles: [
    `
      ::ng-deep table {
        width: 100%;
        margin: 2rem 0;
        border-collapse: collapse;
        font-family: sans-serif;
        box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);

        thead {
          tr {
            background-color: #dd0031;
            color: #ffffff;
            text-align: left;
          }
        }

        tbody tr:hover {
          background-color: #f6f6f6;
        }

        th,
        td {
          padding: 1rem;
        }
      }
    `,
  ],
})
export class AppTableComponent {
  @Input() data!: any[];
  @ContentChild('headers') headers: TemplateRef<any> | undefined;
  @ContentChild('rows') rows: TemplateRef<any> | undefined;
}

So what are we doing?

With the @Input() data property we are passing in the data for the table which will allow our templates to access that data.

We are also using @ContentChild to grab a reference to those templates. We use the names of the templates so that we can access those within those templates.

This is very similar to Angular's @ViewChild which allows us to reference a child object in our template.

2. Using the template

And now we'll create a simple table component that we'll call inventory table.

ng generate component inventory-table

And our code.

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { AppTableComponent } from '../app-table/app-table.component';

@Component({
  selector: 'app-inventory-table',
  standalone: true,
  imports: [CommonModule, AppTableComponent],
  template: `
  <app-table [data]="inventory">
    <ng-template #headers>
      <th>Item</th>
      <th>Price</th>
      <th></th>
      <th></th>
    </ng-template>
    <ng-template #rows let-row>
      <td>{{ row.name }}</td>
      <td>{{ row.price | currency: row.currency }}</td>
      <td>
        <button *ngIf="row.inStock > 0" (click)="purchaseItem(row.plu)">
          Buy now
        </button>
      </td>
      <td>
        <button>Delete</button>
      </td>
    </ng-template>
  </app-table>
`,
})
export class AppInventoryComponent {
  inventory = [
    {
      plu: 110,
      supplier: 'X Corp',
      name: 'Table extender',
      inStock: 500,
      price: 50,
      currency: 'GBP',
    },
    {
      plu: 120,
      supplier: 'X Corp',
      name: 'Heated toilet seat',
      inStock: 0,
      price: 80,
      currency: 'GBP',
    },
    {
      plu: 155,
      supplier: 'Y Corp',
      name: 'Really good pencil',
      inStock: 1,
      price: 8000,
      currency: 'AUD',
    },
  ];

  purchaseItem(plu: number) {
    console.log('handle purchase for', plu);
  }
}

And we should a table that looks something like this.

ngworkspace-content-template-angular-table

Conclusion

You'll notice that ngTemplateOutlet can prove itself very useful in situations where you would otherwise have to provide a lot of weird configurations for your component.

With ngTemplateOutlet the component itself remains a simple and generic component and all of the complexity is handled directly in the implementation component.

If you're interested in learning more about this I highly recommend Joshua Morony's video - ngTemplateOutlet is WAY more useful than I realised.

Till later,

signature

Daniel Kreider