Customize template with ngTemplateOutlet in Angular

Connie Leung - Feb 5 '22 - - Dev Community

Original Post: Customize template with ngTemplateOutlet and ngTemplate in Angular

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>


Enter fullscreen mode Exit fullscreen mode

that is the syntactic sugar of



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


Enter fullscreen mode Exit fullscreen mode

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 123

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>


Enter fullscreen mode Exit fullscreen mode

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



context: { data }


Enter fullscreen mode Exit fullscreen mode

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'
}


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

We can do it

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>


Enter fullscreen mode Exit fullscreen mode


export enum MENU_OPTIONS {
  ALL = 'ALL',
  AVAILABLE = 'AVAILABLE',
  SOLD_OUT = 'SOLD_OUT',
  LOW_SUPPLY = 'LOW_SUPPLY',
}


Enter fullscreen mode Exit fullscreen mode

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.

You are almost there

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


Enter fullscreen mode Exit fullscreen mode


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'
  }
}


Enter fullscreen mode Exit fullscreen mode

Three outcomes:

  • All food is sold out (quantity = 0)

No food

  • All food is available (quantity > 0)

No food is sold out

  • None of the food is low supply

No 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>


Enter fullscreen mode Exit fullscreen mode

We made it to the end

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
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .