Angular: Build more dynamic components with ngTemplateOutlet 🎭

Mustapha Aouas - Oct 15 '19 - - Dev Community

Introduction

To build reusable and developer-friendly components, we need to make them more dynamic (read more adaptable). Great news, Angular comes with some great tools for that. For instance, we could inject content to our components using <ng-content>:



@Component({
  selector: 'child-component',
  template: `
    <div class="child-component">
      <ng-content></ng-content>
    </div>
  `,
})
export class ChildComponent {}

@Component({
  selector: 'parent-component',
  template: `
    <child-component>
      Transcluded content
    </child-component>
  `,
})
export class ParentComponent {}


Enter fullscreen mode Exit fullscreen mode
Snippet 1: Transclusion

Although this transclusion technique is great for simple content projection, what if you want your projected content to be context-aware. For example, while implementing a list component you want the items template to be defined in the parent component while being context-aware (of what is the current item it hosts).
For those kinds of scenarios, Angular comes with a great API called ngTemplateOutlet!

In this post, we will define what ngTemplateOutlet is, then we will build the list component we mentioned above as well as a card component to see the two most common ngTemplateOutlet use-cases. We will do the implementation of these components step-by-step, so by the end of this post, you should feel comfortable using this in your Angular components :)

Definition

From the current Angular documentation ngTemplateOutlet is a directive that: Inserts an embedded view from a prepared TemplateRef.

This directive has two properties:

  • ngTemplateOutlet: the template reference (type: TemplateRef).
  • ngTemplateOutletContext: A context object to attach to the EmbeddedViewRef. Using the key $implicit in the context object will set its value as default.

What this means is that in the child component we can get a template from the parent component and we can inject a context object into this template. We can then use this context object in the parent component.

If you find this too abstract, here is an example of how to use it:



<!-- Child component -->
<child-component>
  <ng-container
    [ngTemplateOutlet]="templateRefFromParentComponent"
    [ngTemplateOutletContext]="{ $implicit: 'Joe', age: 42 }"
  >
  </ng-container>
</child-component>

<!-- Parent component -->
<parent-component [templateRefFromParentComponent]="someTemplate">
  <ng-template #someTemplate let-name let-age="age">
    <p>{{ name }} - {{ age }}</p>
  </ng-template>
</parent-component>


Enter fullscreen mode Exit fullscreen mode
Snippet 2: ngTemplateOutlet usage

In the code above, the child component will have a paragraph containing 'Joe - 42'.
Note that for the name (let-name) we did not specify which property of the context object we had to use because the name was stored in the $implicit property. On the other hand, for the age (let-age="age") we did specify the name of the property to use (in this case it was age).

Well, enough with the definitions. Let's start coding.

The code that will be displayed in this article could be found in this Github repository

Use case #1: Context-aware template

Let's build a list component that takes two inputs from its parent:

  1. data: A list of objects.
  2. itemTemplate: a template that will be used to represent each element of the list.

run ng new templateOutletTutorial --minimal to generate a small Angular project to code along

Let's generate the list component using the Angular schematics (ng g c components/list). Once that's done let's implement the component which will display every item of the data property (the inputted list). On every iteration of the ng-for, it will insert an embedded view that the parent component gives us in the itemTemplate property. While doing so, the component should attach a context object containing the current item.
At the end the list component should look like this:



@Component({
  selector: 'app-list',
  template: `
    <ul class="list">
      <li class="list-item" *ngFor="let item of data">
        <ng-container
          [ngTemplateOutlet]="itemTemplate"
          [ngTemplateOutletContext]="{ $implicit: item }"
        ></ng-container>
      </li>
    </ul>
  `,
  styleUrls: ['list.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ListComponent {
  @Input() data: any[];
  @Input() itemTemplate: TemplateRef<HTMLElement>; // a template reference of a HTML element
}


Enter fullscreen mode Exit fullscreen mode
Snippet 3.1: List component implementation

Then in the parent component, we need to call the list component with a list (of objects) and a template reference:



<app-list
  [itemTemplate]="customItemTemplate"
  [data]="[{ id: 4, name: 'Laptop', rating: 3 },
    { id: 5, name: 'Phone', rating: 4 },
    { id: 6, name: 'Mice', rating: 4 }]"
>
  <ng-template #customItemTemplate let-item>
    <div style="display: flex; justify-content: space-between;">
      <span> {{ item.id }} - <b>{{ item.name }}</b> </span>
      <mark> Stars: {{ item.rating }} </mark>
    </div>
  </ng-template>
</app-list>


Enter fullscreen mode Exit fullscreen mode
Snippet 3.2: Parent component template

Note that we placed the ng-template (item template) inside the app-list component tags. This is only for readability, you could place the item template anywhere you want in the parent template.
Also, I put some inline styles in the item template, but you could also give it a class and style it in the parent component style file.

Use case #2: Template overloading

We saw how ngTemplateOutlet could help us to project context-aware templates, let's see another great use-case: template overloading.

For this, we will build a card component that consists of two parts:

  1. title: A title for the card.
  2. content: The main content of the card.

For the title, we will pass a simple string, and for the content, we can inject it using content projection. Let's do just that after creating the card component with the Angular schematics (ng g c components/card), the component should look like this:



@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <header>{{ title }}</header>
      <article>
        <ng-content></ng-content>
      </article>
    </div>
  `,
  styleUrls: ['card.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CardComponent {
  @Input() title: string;
}


Enter fullscreen mode Exit fullscreen mode
Snippet 4.1: Card component with a string *title*

We call it in the parent component template:



<app-card [title]="'hello there'">
  <p>i'm an awesome card.</p>
</app-card>


Enter fullscreen mode Exit fullscreen mode
Snippet 4.2: Parent component template with a string *title*

Now let's say we want to put an image (<img>) in the title, or use another component in the title template. We would be stuck because the title property only takes a string.
To solve this problem, we could implement a new behavior in our card component. We could say that the title could be a string or a TemplateRef. In case it is a string we will use string interpolation to bind it to the template, otherwise, we will use ngTemplateOutlet.
After implementing the changes, the new card component should then look like this:



@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <header *ngIf="isTitleAString(); else titleTemplateWrapper">{{ title }}</header>
      <ng-template #titleTemplateWrapper>
        <ng-container [ngTemplateOutlet]="title"></ng-container>
      </ng-template>
      <article>
        <ng-content></ng-content>
      </article>
    </div>
  `,
  styleUrls: ['card.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CardComponent {
  @Input() title: string | TemplateRef<HTMLElement>;
  isTitleAString = () => typeof this.title == 'string';
}


Enter fullscreen mode Exit fullscreen mode
Snippet 4.3: Card component with a strTemplateRef *title*

We call it in the parent component template like this:



<app-card [title]="title">
  <ng-template #title> <h2>Hello there</h2> </ng-template>
  <p>i'm an awesome card.</p>
</app-card>


Enter fullscreen mode Exit fullscreen mode
Snippet 4.4: Parent component template with a TemplateRef *title*

Use case #3: Tree

Wrapping up

Wrapping up gif

So, we saw what ngTemplateOutlet is and how we could take advantage of it. We saw 3 of the most common use-cases, but now that you know about this technique maybe you will find another great use-case!


That's it for this post. I hope you liked it. If you did, please share it with your friends and colleagues. Also you can follow me on twitter at @theAngularGuy as it would greatly help me.

Have a good day !


What to read next?

. . . . . . . . . . . . . . . . . . .