Skip to content

Aquent | DEV6

Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages

Using ng-container, ng-template, ngTemplateOutlet

Written by: Pavlo Leheta

Introduction

JavaScript frameworks like Angular allow us to write modular, easier to maintain code as compared to a plain JavaScript. This is achieved through component-based approach where each piece is like a part of a puzzle. Angular features such as directives lets us apply custom functionality, like changing appearance; behaviour of a DOM element; adding and removing DOM elements. Directives like ngIf, and ngFor are commonly used, however, there are also other very helpful, more advanced and useful features in Angular. They are likely not used that often but allow for highly configurable components that are more customized and reusable. Here, we are going to talk about ng-container, ng-template and ngTemplateOutlet.

The ng-template directive and the related ngTemplateOutlet directive support many advanced use cases and are powerful features. They are often used in conjunction with ng-container.

It is beneficial to learn about all of them at once to see how they relate and work together.

The ng-template directive

As the name suggests ng-template is an Angular directive for templating. The content of this directive can be reused throughout Angular component’s template and composed together similarly to how we reuse a piece of code as a part of a function or method. It actually lies in the foundation of Angular’s structural directives such as ngIf, ngFor and ngSwitch. You may have seen ng-template used as a part of if-else block in an Angular template.

<div class="items-list" *ngIf="items else loading">
  ... 
</div>

<ng-template #loading>
    <div>Loading...</div>
</ng-template>

This is a very common use of if/else logic, where the content of ng-template is shown while the dynamic content is being loaded from the backend. The template is referred to by the template reference using #loading and then referred to in the else section of ngIf directive.

Let’s see an example why ng-template lies in the foundation of structural directives by showing ngIf directive’s Angular translation of the example above.

<ng-template [ngIf]="items" [ngIfElse]="loading">
  <div class="items-list">
    ... 
  </div>
</ng-template>

<ng-template #loading>
   <div>Loading...</div>
</ng-template>

The asterisk in front of ngIf is actually “syntactic sugar” and we can see how Angular translates the *ngIf attribute. A similar translation process happens in *ngFor and *ngSwitch directives. It is important to understand that even though ng-template element is declared in our template, it will only be present in the DOM when Angular consumes it. In the example above, it will only be shown for a period of time when items are being loaded. Otherwise it is just shown as a comment. For example:

<!--bindings={
  "ng-reflect-ng-if": "1,2,3"
}-->

The content of items object is 1, 2, 3. This is why it is present in the comments.

The ng-container element

What if we need to apply more than one structural directive to the same element in our template?

<div class="items-list" *ngIf="items"
*ngFor="let item of items">
    <p>{{item}}</p>
  </div>

We won’t be able to do so and get the following error:

Error: Template parse errors:

Can’t have multiple template bindings on one element. Use only one attribute prefixed with *…

This is for simplicity. Because structural directives can do complex things with elements they are applied to, it is hard to figure out which directive should be applied first, or they cancel each other’s effects. A solution to this use case can be an introduction of another element where one of the directives is applied.

  <div *ngIf="items">
    <div class="items-list" *ngFor="let item of items">
      <p>{{item}}</p>
    </div>
  </div>

As you can see, it will introduce an unnecessary div element into our resultant DOM. This may be a problem if our list is long — we’ll end up with so many extra DOM elements which do nothing but take up space. Another problem could be a case where specific HTML elements have to be wrapped by a parent element. For instance, select element has to have an option element as a direct child and option cannot be additionally wrapped in a div or other element. Similarly, when we have a table element, it has to have a certain order of its child elements.

We could also break our styling if extra wrapping elements were introduced into our elements tree. Angular has a solution to this: using the ng-container grouping element. This element does not interfere with styling or layout because it’s not being put into the DOM by Angular and is only recognized by the Angular parser. It is not a directive, component or class. It acts similar to curly braces for an if statement by encompassing the inner content. By utilizing ng-container, the above example could be written as follows:

<ng-container *ngIf="items">
  <div class="items-list" *ngFor="let item of items">
    <p>{{item}}</p>
  </div>
</ng-container>

In this case one div element will be added to the DOM for each item when items are present.

Another major use case for ng-container is that it can serve as a placeholder for a dynamic injection of a template into the page.

The ngTemplateOutlet directive

We saw how ng-template is used in the if/else statement of *ngIf directive. As I mentioned, we can reuse ng-templates similarly to how we reuse functions or methods. This is achieved with a help of ngTemplateOutlet directive. Remember the example with loading a template? We could inject it anywhere in our page by using ng-container with an applied ngTemplateOutlet directive like so:

<ng-container *ngTemplateOutlet="loading"></ng-container>
<ng-template #loading>
  <div>Loading...</div>
</ng-template>

We could also use ngTemplateOutlet directive to dynamically insert an embedded view from a prepared TemplateRef defined in our component’s class. This would make our component highly configurable. For example:

@Component({
  selector: 'app-component',
  template: `
    <ng-container *ngTemplateOutlet="customTemplate">
</ng-container>
  `
})
export class AppComponent {

  @Input() customTemplate: TemplateRef<any>;
}

When we use ng-template section in our component’s template, all the inner scope of it has access to outer scope’s variables. Angular lets us to have even more flexibility through the ngTemplateOutlet directive by providing a custom context object to it which provides template specific input variables like so:

@Component({
  selector: 'app-component',
  template: `      
    <ng-template #dayTemplate let-day let-date="todaysDate">
        <div>Today is {{day}}, {{date}}</div>
    </ng-template>
    <ng-container 
      *ngTemplateOutlet="dayTemplate; context: dayContext">
    </ng-container>
`})
export class AppComponent {

    @Input() day: string;
    @Input() date: string;
    
    dayContext = {$implicit: this.day, todaysDate: this.date};
  
}

Using the $implicit key in the context object serves as the context’s default value. We have 2 input variables declared for the #dayTemplate template which provide input to its inner scope (can be used in the scope in between ng-template opening and closing tags). In order to define the input variable for template, the let- prefix is used.

The variable day declared as let-day, is used with $implicit key in the context object of the component’s class. It does not need to be provided with a specific key as compared to the other variable date where we assign todaysDate from the context object dayContext in our component’s class. The day and date variables are not accessible in the component template’s scope outside of ng-template, meaning is you try to reference them from ng-container element, you can’t. Angular will translate our ngTemplateOutlet directive as follows:

<ng-container
  [ngTemplateOutlet]="dayTemplate"             
  [ngTemplateOutletContext]="dayContext">
</ng-container>

Configurable Component Example

Now we will take a look at a real life reusable component. Here is a partial implementation of a resizable splitter component which allows to split a screen by 2 parts horizontally or vertically. We can grab and drag the divider element and resize each split frame. It shows how ng-template, ng-container and ngTemplateOutlet can be utilized to create a configurable component. The example also shows how to use context with ngTemplateOutlet.

@Component({
  selector: 'app-resizable-splitter',
  template: `
    <div class="splitter" [ngClass]="{'vertical': isVertical, 'horizontal': !isVertical}" #container>
      <div class="frame" #frame1Container>
        <ng-container *ngTemplateOutlet="firstFrame; context: firstFrameContext"></ng-container>
      </div>
      <div class="handle" #handle (mousedown)='onHandlePress()'>
        <div class="handle-area"></div>
      </div>
      <div class="frame" #frame2Container>
        <ng-container *ngTemplateOutlet="secondFrame; context: secondFrameContext"></ng-container>
      </div>
    </div>
  `,
  styleUrls: ['./resizable-splitter.component.scss']
})

export class ResizableSplitterComponent implements OnInit, AfterViewInit, OnDestroy {

  @Input() isVertical = true;
  @Input() firstFrame: TemplateRef<any>;
  @Input() secondFrame: TemplateRef<any>;
  @Input() firstFrameSize = 50; // in percentage
  @Input() firstFrameContext: any = {};
  @Input() secondFrameContext: any = {};

. . .
}

In a nutshell, this component lets us input 2 frames or separate views as ng-templates by using ngTemplateOutlet directive on ng-container element. It then uses ElementRef references to resize each of 2 frames when a user presses on a divider element with a mouse and drags the bar.

This is how the element is used in a parent’s template.

<app-resizable-splitter
  [firstFrame]="top"
  [secondFrame]="bottom"
  [isVertical]="true">
</app-resizable-splitter>
<ng-template #top>
  <app-resizable-splitter
    [firstFrame]="topLeft"
    [secondFrame]="topRight"
    [isVertical]="false"
    [firstFrameSize]="33"
    [firstFrameContext]="topLeftContext"
    [secondFrameContext]="topRightContext"></app-resizable-splitter>
</ng-template>
<ng-template #bottom>
  <!-- content of bottom half -->
  <div style="background-color: #8BBEE8FF;" class="full-size">
    <p>Bottom section</p> 
  </div>
</ng-template>
<ng-template #topLeft let-colour="colour">
  <div [ngStyle]="{ 'background-color': colour }" class="full-size">
    <p>Top left section</p> 
  </div>
</ng-template>
<ng-template #topRight let-x>
  <div [ngStyle]="{ 'background-color': x }" class="full-size">
    <p>Top right section</p> 
  </div>
</ng-template>

And this is the parent’s class file:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'resizable-splitter';

  topLeftContext = {
    colour: '#D7A9E3FF'
  };

  topRightContext = {
    $implicit: '#A8D5BAFF'
  };
}

This example uses 2 instances of the resizable splitter component. First instance splits the document horizontally and the second one is used to split the first frame of the first instance vertically. Each frame is defined by its own ng-template. For instance, reference variable #top, #bottom helps to identify templates for top and bottom frames respectively which then passed into our component as inputs ([firstFrame], [secondFrame]). Note how background color is set for our templates. The #bottom template has it set via inline styling whereas the other 2 use ngTemplateOutlet’s context to pass color values to our component. Specifically, there are topLeftContext and topRightContext objects defined in our class which are passed into the resizable splitter component via [firstFrameContext] and [secondFrameContext] inputs respectively.

This is how it would look in a browser:

How it looks in the browser (3 tiles being dragged to different sizes)

Summary

Angular directives ng-template, ngTemplateOutlet and the ng-container grouping element in a combination give us an opportunity to create dynamic, highly configurable and customized components. They complement other structural directives and features that Angular offers to help developers deliver robust, flexible, maintainable enterprise grade applications. My intention was to familiarize you with these more advanced Angular concepts and provide examples so you have a good understanding of them and can start using them as a part of your toolset.