ngTemplateOutlet: The secret to customisation

Post Editor

In this article we demonstrate how to use ngTemplateOutlet, along with ngTemplateOutletContext, to make a component completely customisable.

10 min read
1 comment
post

ngTemplateOutlet: The secret to customisation

In this article we demonstrate how to use ngTemplateOutlet, along with ngTemplateOutletContext, to make a component completely customisable.

post
post
10 min read
1 comment
1 comment

ngTemplateOutlet is a powerful tool for creating customisable components. It is used by many Angular libraries to enable users to provide custom templates. But how can we do this for our own components?

In this article we demonstrate how to use ngTemplateOutlet, along with ngTemplateOutletContext, to make a component completely customisable.

Customising a Dropdown Selector
Link to this section

We will be working with a dropdown selector as it serves as a great use case for customising a component with ngTemplateOutlet . Our dropdown selector is used by multiple clients (my two shark obsessed children) who each have a number of outstanding feature requests. Let's start by introducing our component code and then start adding new features.

(If you want to skip to the end, the final customisable selector is live here).

Selector Component
Link to this section

Our selector begins with a clean api. It takes a list of strings and displays those via ngFor in a dropdown.

<>Copy
export class SelectorComponent { selected: string; @Input() options: string[]; @Output() selectionChanged = new EventEmitter<string>(); selectOption(option: string) { this.selected = option; this.selectionChanged.emit(option); } } // selector.component.ts

All looks good so far with a clean component interface.

<>Copy
<div dropdown> <button dropdownToggle>{{selected || 'Select'}}</button> <ul dropdownMenu> <li *ngFor="let option of options" (click)="selectOption(option)"> {{option}} </li> </ul> </div> <!-- selector.component.html -->

Using this component our first client can select their favourite shark.

<>Copy
<app-selector [options]="sharks"></app-selector> <!-- client-one.component.html -->
First Shark Dropdown
Initial string selector

Feature: Customise Option Text
Link to this section

Our second client, who also likes sharks, wants to include the Latin name in their dropdown menu. We could make a small change by adding a display call-back function as an Input to update the displayed text. This is not necessarily recommended.

<>Copy
@Input() displayFunc: (string) => string = x => x; // selector.component.ts
<>Copy
<li *ngFor="let option of options"> <!-- Pass the option through the display callback --> {{displayFunc(option)}} </li> <!-- selector.component.html -->
<>Copy
<app-selector [options]="sharks" [displayFunc]="appendLatin"> </app-selector> <!-- client-two.component.html -->
Reminder: This is not the recommended approach
Sharks with Latin names
Using a display callback to update the text content

Feature: Safe To Swim Icon
Link to this section

Client one now wants to include an icon depicting whether a shark is safe to swim with. They provide us with their hefty icon package which we have to use. How are we going to support this?

Unlike the previous request, which just changed the text content, adding in an icon will require structural changes to our HTML template.

Wrong approach using *ngIf
Link to this section

Without knowing about ngTemplateOutlet we could decide to use *ngIf and another callback that provides the icon name based on the current shark.

<>Copy
<li *ngFor="let option of options"> <!-- Introducing the icon into our selector --> <c1-icon *ngIf="getIconFunc(option)" [name]="getIconFunc(option)" /> {{displayFunc(option)}} </li> <!-- selector.component.html -->

If no icon callback is provided the default returns undefined to hide the icon via our ngIf. This ensures that our other clients do not see these icons.

<>Copy
@Input() getIconFunc: (string) => string = x => undefined; // selector.component.ts
<>Copy
<app-selector [options]="sharks" [getIconFunc]="getIconFunc"> </app-selector> <!-- client-one.component.html -->

This works and enables them to have the following selector but let's not get too excited because this was not a great solution.

Dropdown with icons
Sub-optimal solution using ngIf

Unhappy Client due to Icon dependency
Link to this section

In the previous feature request we introduced a dependency on client one's icon package. This is really bad! Consider forcing other clients to install an extra dependency to compile their applications even though they will never actually require the package.

You may consider your best option is to fork the component and have a separate instance for each client. While this may be a quick fix for client two, it now means you have multiple dropdown selectors to support. Not a happy position as a developer!

What about using ng-content?
Link to this section

In Angular we can use <ng-content> to perform content projection. Perhaps we could replace the icon in the template with a <ng-content> and have client one project their icon into our selector. This way we can remove the icon dependency from our component.

<>Copy
<li *ngFor="let option of options"> <!-- Removed: <c1-icon [name]="swimIcon(option)" /> --> <ng-content></ng-content> {{displayFunc(option)}} </li> <!-- selector.component.html -->
<>Copy
<app-selector [options]="sharks"> <c1-icon [name]="swimIcon(????)" /> </app-selector> <!-- client-one.component.html -->

While this looks promising it will not work. The icon will only be displayed for the last item in the list. You can only project content into a single location unless you use named slots. There is no easy way to dynamically name slots like in our list above.

ng-content does not work
ng-content does not work for this use case

The main issue is that <ng-content> is not aware of the context where it is being rendered. It does not know the shark option it is being used for. This means that we cannot customise its content based on the dropdown value.

If only there was a way for us to project a template into our component that was also aware of its local context. This is where ngTemplateOutlet comes in!

NgTemplateOutlet
Link to this section

ngTemplateOutlet acts as a placeholder to render a template after providing that template with context. In our case we want a template placeholder for each dropdown option and the context would be the shark.

The Angular documentation for ngTemplateOutlet is currently a little lacking. This issue has been raised and ideas on how to demonstrate the feature have started being shared.

Defining a Template
Link to this section

Before we can use ngTemplateOutlet we must first define a template using <ng-template>. The template is the body of the <ng-template> element.

<>Copy
<ng-template #myTemplate> <div>Hello template</div> </ng-template>

To reference the template we name it via # syntax. By adding #myTemplate to the element we can get a reference to the template using the name myTemplate . The type of myTemplate is TemplateRef.

Rendering a Template
Link to this section

The content of a <ng-Template> element is not rendered in the browser. To have the template body rendered we must now pass the template reference to a ngTemplateOutlet.

<>Copy
<!-- Define our template --> <ng-template #myTemplate> World! </ng-template> Hello <!-- Render the template in this outlet --> <ng-container [ngTemplateOutlet]="myTemplate"></ng-container>
Hello World Template
Rendered output of our template and outlet

ng-template and ngTemplateOutlet enable us to define re-usable templates which in itself is a powerful feature but we are just getting started!

Supplying the Template Context
Link to this section

We can take templates to the next level by supplying a context. This enables us to pass data to the template. In our case the data is the shark for the current option. To pass context to a template you use [ngTemplateOutletContext].

Here we are passing each dropdown option to the optionTemplate. This will enable the option template to display a different value for each item in the list. We are also setting the current index to the idx property of our context as this can be useful for styling.

<>Copy
<li *ngFor="let item of items; index as i"> <!-- Setting the option as the $implicit property of our context along with the row index --> <ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }" ></ng-container> </li> <!-- selector.component.html -->

You can also use the abbreviated syntax below.

<>Copy
<!-- Alternative syntax --> <ng-container *ngTemplateOutlet="optionTemplate; context:{ $implicit: option, idx: i }" ></ng-container>

Using the Context in your template
Link to this section

To access the context in our template we use let-* syntax to define template input variables. To bind the $implicit property to a template variable called option we add let-option to our template. We can use any name for our template variable so let-item or let-shark would also bind to the $implicit property in the context.

This enables us to define a template outside of the selector component but with access to current option just as if our template was defined in the dropdown itself!

<>Copy
<ng-template #optionTemplate let-option let-position="idx"> {{ position }} : {{option}} </ng-template> <!-- client-one.component.html -->

To access the other properties on our context we have to be more explicit. To bind the idx value to a template variable called position we add let-position=idx. Alternatively we could name it id by adding let-id=idx.

Note that we must know the exact property name when extracting values from the context that are not the $implicit property. The $implicit property is a handy tool which means users do not have to be aware of this name as well as having to write less code.

Library authors please add the type structure of your context to your documentation! There is currently no auto-complete / type checking available for template input variables.

By using template input variables we are able to combine state from where we define the template, with the context provided to us where the template is instantiated. This provides us with some amazing capabilities!

Solving our feature requests
Link to this section

We are now in a position to solve our clashing client demands with a single selector. As a reminder, our first client wanted their custom icon in the dropdown while our second client, justifiably, did not want that dependency.

Setup the template outlet in our component
Link to this section

To use a template within our app-selector we replace the display function and icon element with a ng-container containing a ngTemplateOutlet . This outlet will either use the user's optionTemplate or our defaultTemplate if no template is provided by the user.

<>Copy
<li *ngFor="let option of options; index as i"> <!-- Define a default template --> <ng-template #defaultTemplate let-option>{{ option }}</ng-template> <ng-container [ngTemplateOutlet]="optionTemplate || defaultTemplate" [ngTemplateOutletContext]="{ $implicit: option, index: i}" > </ng-container> </li> <!-- selector.component.html -->
Default templates are a great way to retro-fit ngTemplateOutlet to an existing component.

To ensure that the template will be able to display the current option we must remember to setup the context. We set the option to be the $implicit property and also provide the current row index.

The component will accept the optionTemplate via an Input.

<>Copy
@Input() optionTemplate: TemplateRef<any>; // selector.component.ts
You can also use @ContentChild to pass the template into your component. This forces the template to be defined within the <app-selector> which may be preferred if you have a lot of Input properties. However, this does make it harder to share templates across multiple component instances.

Define the client template
Link to this section

Now we can define our custom template in client one's codebase. Here we use the template input variable to ensure we display the correct icon for the given shark.

<>Copy
<ng-template #sharkTemplate let-shark> <c1-icon name="{{ getIconFunc(shark) }}" /> {{ shark }} </ng-template> <!-- Pass sharkTemplate to our selector via an Input --> <app-selector [options]="sharks" [optionTemplate]="sharkTemplate" ></app-selector> <!-- client-one.component.html -->

We then pass our template by reference into the component via the optionTemplate @Input.

This all results in our final shark selector meeting client one's requests and at the same time ensuring no other clients require the icon dependency any more.

Final Client 1 Dropdown
Customised selector using template outlet

Tractors instead of sharks
Link to this section

Just when we thought we were finished client two comes back to us with the exciting news that they don't like sharks anymore and instead they love tractors! They now want a dropdown to pick tractors with pictures and buttons.

The great thing is that we can give them whatever they want without changing any of our selector code. This is the beauty and power of ngTemplateOutlet.

We just update the template use in client two's code base for tractors and pass that in.

<>Copy
<ng-template #tractorTemplate let-tractor> <label>{{ tractor.name }}</label> <img src="{{ tractor.img }}" /> <button>Buy Now!</button> </ng-template> <!-- No change to selector for brand new dropdown style --> <app-selector [options]="tractors" [optionTemplate]="tractorTemplate" ></app-selector> <!-- client-two.component.html -->
Final Client 2 Dropdown
Same selector but totally different client template

Final Selector Code
Link to this section

By using ngTemplateOutlet we are able to seperate the work of being a selector from the user customisations. This enables us to maintain a minimal component api without restricting our clients' creativity.

<>Copy
export class SelectorComponent<T> { @Input() options: T[]; @ContentChild("optionTemplate") optionTemplateRef?: TemplateRef<any>; @Output() selectionChanged = new EventEmitter<T>(); } // selector.component.ts
<>Copy
<li *ngFor="let option of options; index as i"> <ng-template #defaultTemplate let-option>{{ option }}</ng-template> <ng-container [ngTemplateOutlet]="optionTemplate || defaultTemplate" [ngTemplateOutletContext]="{ $implicit: option, index: i}" > </ng-container> </li> <!-- selector.component.html -->

Conclusion
Link to this section

I hope that after reading this article you will be able to use ngTemplateOutlet to support template customisations in your own components! I also hope you will have a deeper understanding of how your favourite component libraries are using ngTemplateOutlet to enable you to customise them.

Further Reading
Link to this section

Here I have covered a single use case for ngTemplateOutlet. If you liked this article then I would strongly recommend reading Alex Inkin's article Agnostic components in Angular which takes things even further.

Live Example
Link to this section

Experiment for yourself with this live example on Stackblitz or clone the repo StephenCooper/ngTemplateOutlets from GitHub.

If you prefer watching videos you can see me present this article at Angular Connect 2019.

Comments (1)

authorTomerAgmon
16 September 2021

Nice project but I have a small issue with it. You use an element id to identify the options template (#optionName) rather than using a directive. By using a directive you eliminate the possibility of miss-spelling the template identifier (which would break the app if the developer does that). Also a question - you're showing two ways to do content projection. What's the difference between them? why should we use one and not the other? They seem to do pretty much the same

Share

About the author

author_image

Stephen is a Senior Engineer at G-Research specialising in web technologies. When not coding you will find him out and about with his three little explorers.

author_image

About the author

Stephen Cooper

Stephen is a Senior Engineer at G-Research specialising in web technologies. When not coding you will find him out and about with his three little explorers.

About the author

author_image

Stephen is a Senior Engineer at G-Research specialising in web technologies. When not coding you will find him out and about with his three little explorers.

Looking for a JS job?
Job logo
PDQ team| Senior JavaScript developer (Angular/Node)

SD Solutions

Ukraine
Remote
$60k - $80k
Job logo
Senior Full-Stack Developer (Node+Angular)

A-Listware

Ukraine
Remote
$48k - $78k
Job logo
Senior Full stack (Angular+Node)

Monolith

Ukraine
Remote
$60k - $84k
Job logo
AngularJS Developer/.net Core - Remote Contract

InfoMagnus

United States
Remote
$115k - $134k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
AngularpostTracking user interaction area

13 September 2021

8 min read

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
Angularpost
7 September 202122 min read
Designing Angular architecture - Container-Presentation pattern

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more
micro frontendspostTaking micro-frontends to the next level

6 September 2021

25 min read

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more