Customize template with ngTemplateOutlet and ngTemplate in Angular

Reading Time: 4 minutes

Loading

Introduction

When Angular components require to render ngTemplates programmatically, ngif-then-else construct takes care of most of the scenarios. However, ngIf is lack of passing context that ngTemplateOutlet directive supports. If either template depends on inputs or calculated data of component, then we can pass the values to them via template context of ngTemplateOutlet directive.

The usage of ngTemplateOutlet is shown as follows:

<ng-container *ngTemplateOutlet="templateRefExp; context: contextExp"></ng-container>

that is the syntactic sugar of

<ng-container [ngTemplateOutlet]="templateRefExp" [ngTemplateOutletContext]="contextExp"></ng-container>

In this post, we learn how to use ngTemplateOutlet directive in a <ng-container> element, assign different templates to the directive given the result of the ternary expression. We can supply inputs to template context and the rendered ngTemplate can use the data in the context to render the content subsequently.

let's go

Final output after using ngTemplateOutlet

Customize ngContainer to host ngTemplateOutlet

First, we add <ng-container> element in food-menu.component.html to host a ngTemplateOutlet directive. The directive receives an instance of ngTemplate based on the result of the ternary expression. When the expression is true, the directive gets “hasFood” template. On the other hand, it gets “noFood” template when the expression is false.

<ng-container *ngTemplateOutlet="data.menuItems.length > 0 ? hasFood : noFood; context: { data }"></ng-container>

Moreover, we pass the data object to the template context for both templates to access its values.

context: { data }

For you information, data is an object that has two properties: menuItems and option. MenuItems is an array that stores the information of menu items and their choices. Option stores the selected value of the dropdown.

data: {
   menuItems: [ 
     { question: '...', choices: [...] }, 
     { question: '...', choices: [...] } 
   ],
   option: 'AVAILABLE'
}

Define hasFood ngTemplate to assign to the ngTemplateOutlet directive

Then, we define hasFood template that is displayed when the condition, data.menuItems.length > 0, is met.

Since ngTemplateOutlet has a context expression, let-data="data" allows us to access the data object in the context. Next, we iterate the array to display each menu item in <app-food-menu-card> component. <app-food-question> prompts user to select food with a question while <app-food-choice> provides an input field to enter quantity to order.

<ng-template #hasFood let-data="data">
  <app-food-menu-card *ngFor="let menuItem of data.menuItems; index as i; trackBy: menuItemTrackByFn">
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice
        [choice]="choice"
        [qtyMap]="qtyMap"
        (foodChoiceAdded)="handleFoodChoiceSub$.next($event)"
      ></app-food-choice>
    </ng-container>
  </app-food-menu-card>
</ng-template>
we can fix all accessibility violations

Define noFood ngTemplate to assign to the ngTemplateOutlet directive

First ngTemplate is ready and we need to create the second ngTemplate, noFood. This template shows a simple text when the menuItems array has no item.

<ng-template #noFood let-data="data">No food or drink that is {{ data.option | renderMenuOption }}.</ng-template>
export enum MENU_OPTIONS {
  ALL = 'ALL',
  AVAILABLE = 'AVAILABLE',
  SOLD_OUT = 'SOLD_OUT',
  LOW_SUPPLY = 'LOW_SUPPLY',
}

If you are curious of data.option, it is a value of MENU_OPTIONS enum. The enum has four member values: ‘ALL’, ‘AVAILABLE’, ‘LOW_SUPPLY’ or ‘SOLD_OUT’ that are in uppercase. Due to the casing and underscore format of the member values, we will create a custom pipe to transform the value to normal English words.

Build custom pipe to transform value in ngTemplate noFood

Finally, use Angular CLI to generate the boilerplate code for the custom pipe

ng g pipe RenderOptionPipe
import { Pipe, PipeTransform } from '@angular/core'

import { MENU_OPTIONS } from '../enums'

@Pipe({
  name: 'renderMenuOption',
})
export class RenderOptionPipe implements PipeTransform {
  transform(value: MENU_OPTIONS): string {
    if (value === MENU_OPTIONS.AVAILABLE) {
      return 'available'
    } else if (value === MENU_OPTIONS.LOW_SUPPLY) {
      return 'low supply'
    }

    return 'sold out'
  }
}

Three outcomes:

  • All food is sold out (quantity = 0)
  • All food is available (quantity > 0)
  • None of the food is low supply

Final code in template

<div class="food-menu" *ngIf="menuItems$ | async as data; else notAvailable">
  <app-food-menu-option (menuOptionSelected)="menuOptionSub$.next($event)"></app-food-menu-option>
  <ng-container *ngTemplateOutlet="data.menuItems.length > 0 ? hasFood : noFood; context: { data }"></ng-container>
</div>

<ng-template #notAvailable>No menu</ng-template>
<ng-template #hasFood let-data="data">
  <app-food-menu-card *ngFor="let menuItem of data.menuItems; index as i; trackBy: menuItemTrackByFn">
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice
        [choice]="choice"
        [qtyMap]="qtyMap"
        (foodChoiceAdded)="handleFoodChoiceSub$.next($event)"
      ></app-food-choice>
    </ng-container>
  </app-food-menu-card>
</ng-template>
<ng-template #noFood let-data="data">No food or drink that is {{ data.option | renderMenuOption }}.</ng-template>

Final thoughts

When a component requires to render conditional templates, ngIf may not be the right approach especially when the templates expect inputs from the component. A robust solution is to host ngTemplateOutlet directive in ng-container element, and assign templates and context to the directive in a ternary expression.

The result of the ternary expression controls which template to display; the template can access variables in the template context and use the values in elements.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. ngTemplateOutlet documentation: https://angular.io/api/common/NgTemplateOutlet
  3. ngTemplateOutput: The secret to customization: https://indepth.dev/posts/1405/ngtemplateoutlet