tldr;
ng-template
, ng-container
, and ngTemplateOutlet
provide the ability to reuse content inside an Angular component. Understanding what’s going on and how to use these methods allows a developer to build what they want without needing to duplicate parts of the template.
Explanation of the Problem
Have you ever needed to use *ngIf
in your template, and if the expression evaluates to false use a backup template? On top of that, maybe both situations (if the expression is true or false) require the same layout inside of the element that has the *ngIf
directive on it.
I recently had came across this situation. We had some data that was being output in a list either had a link that stayed internal to the app or opened a link in a new tab external to the application. The content inside the links, however, always had the same layout. I didn’t want to duplicate the inner content, and luckily Angular provides the ability to accomplish this. The method includes using ng-template
, ng-container
, and ngTemplateOutlet
.
Before getting to the end solution, let’s look at an example of the data and how it’s output without using ng-template
.
export class ListComponent {
links = [
{ internal: true, display: "Home", url: "/" },
{ internal: false, display: "External", url: "https://test.com" },
];
}
<ul>
<li *ngFor="let link of links">
<a *ngIf="link.internal" [routerLink]="link.url">
<img src="/assets/icon.svg" alt="">
<span>{{ link.display }}</span>
</a>
<a *ngIf="!link.internal" [attr.href]="link.url">
<img src="/assets/icon.svg" alt="">
<span>{{ link.display }}</span>
</a>
</li>
</ul>
In this method, we have two *ngIf
directives on two separate a
tags, and the inner content of the link is the same in both cases. If that inner content changes, we have to make the change to both places or else there are inconsistencies in the layout. This is not ideal; we are repeating ourselves and there are multiple locations where we can introduce bugs.
Solution
Angular provides a much more elegant solution to our problem. Our TypeScript code from above will remain the same, but we can have a lot less duplication in the HTML by using a couple special Angular tags. First I’ll show the HTML, and then we’ll discuss the different pieces.
<ul>
<li *ngFor="let link of links">
<a *ngIf="link.internal; else externalLink" [routerLink]="link.url">
<ng-container
[ngTemplateOutlet]="linkLayout"
[ngTemplateOutletContext]="{ link: link }"
></ng-container>
</a>
<ng-template #externalLink>
<a [attr.href]="link.url">
<ng-container
[ngTemplateOutlet]="linkLayout"
[ngTemplateOutletContext]="{ link: link }"
></ng-container>
</a>
</ng-template>
</li>
</ul>
<ng-template #linkLayout let-link="link">
<img src="/assets/icon.svg" alt="" />
<span>{{ link.display }}</span>
</ng-template>
There’s a lot going on here, so let’s break it down. We’ll start with the *ngIf
on the a
tag.
<a *ngIf="link.internal; else externalLink" [routerLink]="link.url">
<ng-container
[ngTemplateOutlet]="linkLayout"
[ngTemplateOutletContext]="{ link: link }"
></ng-container>
</a>
<ng-template #externalLink>
<a [attr.href]="link.url">
<ng-container
[ngTemplateOutlet]="linkLayout"
[ngTemplateOutletContext]="{ link: link }"
></ng-container>
</a>
</ng-template>
The *ngIf
directive has an expression listed: if link.internal
evaluates to true
, then show this a
tag. If it’s false, output the ng-template
tag that’s referenced by the externalLink
local template variable. This is how we can either navigate to a new route that’s part of the Angular app or external to the app. This is a method that can be used in any situation where your *ngIf
needs an else
clause.
Next up, let’s look at what’s going on with the ng-container
inside both of the a
tags in the example.
<ng-container
[ngTemplateOutlet]="linkLayout"
[ngTemplateOutletContext]="{ link: link }"
></ng-container>
You can read more about the ng-container
element in the Angular docs, but at a high level it’s a special element that can hold structural directives without adding new elements to the DOM. It can also be used in situations like this, with the ngTemplateOutlet
structural directive. The ngTemplateOutlet
directive determines what ng-template
will be output on the page inside the ng-container
. The context for the template, or needed variables, can be declared with the ngTemplateOutletContext
directive. In the above case, we are saying that the #linkLayout
content should be output on the page, and that there should be a variable called link
that will be used inside the ng-template
. The contents of the ng-template tag can be placed anywhere in the HTML file, though I’ve placed them at the bottom in this example.
<ng-template #linkLayout let-link="link">
<img src="/assets/icon.svg" alt="" />
<span>{{ link.display }}</span>
</ng-template>
This template will be output on the screen anywhere it’s referenced. In this case, inside both a
tags. If we want to change the layout inside the a
tags, we need only change this one spot in the HTML. Everything will be kept in sync, and we have fewer possibilities of inconsistencies in the component.
There is one alternate way of declaring the ngTemplateOutlet
. The result will be the same as the method shown above.
<ng-container
*ngTemplateOutlet="linkLayout; context: { link: link }"
></ng-container>
Conclusion
The above method limits the duplication to a certain degree, especially for the content inside the a
tag. The example here is a fairly simple layout. This method becomes more useful when the internal layout becomes more complicated. That inner layout is defined once and reused in multiple locations. You could create a new component if you wanted, but that can introduce complexity sometimes as well.
Knowing how to use ng-template
, ng-container
, and ngTemplateOutlet
can allow you to effectively output the content of a component with minimal duplication
Top comments (0)