Rendering A List Of Mixed Types Using NgFor And NgTemplateOutlet In Angular 9.0.0-rc.5
Earlier this year, I looked at rendering a list of mixed-types using ngFor
and ngSwitch
in Angular 7. The technique outlined in that post is the technique that I generally use. However, as of late, I've been doing a lot of experimentation with various use-cases for ng-template
; and, it recently occurred to me that I could also render a list of mixed data-types usingng-template
and ngTemplateOutlet
. But, I wasn't sure how this would "feel"; so, I wanted to put together a quick demo in Angular 9.0.0-rc.5.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
In Angular, we can use ng-template
to either define a TemplateRef
instance; or to render a TemplateRef
via the ngTemplateOutlet
directive. In fact, we can use the two use-cases together to create some pretty powerful functionality, such as the recursive rendering of ng-template
fragments.
For this blog post, I'm going to combine the two use-cases to render a ngFor
list of mixed data-types. But, before we look at the code, let's take a minute to understand how Boolean operators work in JavaScript, as that is the key-driver of this technique.
The And operator - &&
- will evaluate a list of expressions and short-circuit on the first Falsy value. The result of the overall expression is then the last evaluated sub-expression. Therefore, the following expression:
true && 1 && 0 && true
... results in 0
as this is the last-evaluated sub-expression that short-circuits the overall expression.
Similarly, the Or operator - ||
- will evaluate a list of expressions and short-circuit on the first Truthy value. And, just as with the And operator, the result of the overall expression is then the last evaluated sub-expression. Therefore, the following expression:
false || 0 || 1 || true
... results in 1
as this is the last-evaluated sub-expression that short-circuits the overall expression.
ASIDE: The evaluation of Boolean statements is an absolutely fundamental part of JavaScript. If the above examples don't feel natural for you, I recommend that check out the Mozilla Developer Network doc on Logical operators. Honestly, that page is far more interesting (and more important) than my blog post :P
In my demo, I'm using this type of Boolean evaluation to determine which TemplateRef
to render in a given ngTemplateOutlet
. To set this up, let's quickly look at how our data is being defined. Here, in my App component, I have a mixture of InVision projects that contain both Prototypes and Boards:
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface Prototype {
type: "prototype";
id: string;
name: string; // This property is unique to this type.
}
interface Board {
type: "board";
id: string;
title: string; // This property is unique to this type.
}
@Component({
selector: "app-root",
styleUrls: [ "./app.component.less" ],
templateUrl: "./app.component.html"
})
export class AppComponent {
public projects: ( Prototype | Board )[];
// I initialize the app component.
constructor() {
this.projects = [
{ type: "prototype", id: "p1", name: "P1" },
{ type: "prototype", id: "p2", name: "P2" },
{ type: "board", id: "b1", title: "B1" },
{ type: "prototype", id: "p3", name: "P3" },
{ type: "board", id: "b2", title: "B2" }
];
}
}
As you can see, the Prototype and Board data-types each contain a different set of properties. However, we are going to render them both in a single list. As such, we will have to differentiate which project-type is being rendered in each list item iteration.
In the App component's template, I am rendering the list of projects twice: first using my "traditional" approach of ngSwitch
; and the second using ngTemplateOutlet
:
<h2>
Projects (Using NgSwitch)
</h2>
<ul>
<li
*ngFor="let project of projects"
[ngSwitch]="project.type">
<ng-template [ngSwitchCase]="( 'prototype' )">
{{ project.name }}
</ng-template>
<ng-template [ngSwitchCase]="( 'board' )">
{{ project.title }}
</ng-template>
</li>
</ul>
<h2>
Projects (Using NgTemplateOutlet)
</h2>
<ul>
<ng-template #prototypeRef let-prototype>
{{ prototype.name }}
</ng-template>
<ng-template #boardRef let-board>
{{ board.title }}
</ng-template>
<li *ngFor="let project of projects">
<ng-template
[ngTemplateOutlet]="(
( ( project.type === 'prototype' ) && prototypeRef ) ||
( ( project.type === 'board' ) && boardRef )
)"
[ngTemplateOutletContext]="{ $implicit: project }">
</ng-template>
</li>
</ul>
When using ngTemplateOutlet
, notice that the individual TemplateRef
fragments are defined outside of the actual ngFor
. Then, within the ngFor
, I am using ng-template
with the ngTemplateOutlet
directive to select the appropriate TemplateRef
based on the given type. This is where that Boolean expression evaluation comes into play:
(
( ( project.type === 'prototype' ) && prototypeRef ) ||
( ( project.type === 'board' ) && boardRef )
)
In this case, I am using type
and strict-equality operator to short-circuit the expression. This means that Angular will use whichever TemplateRef
value follows the first matching equality check (which then short-circuits the overall expression).
And, when we run this Angular code, we get the following browser output:
As you can see, both approaches to rendering mixed data-types using ngFor
work as expected. But, when we look at the underlying markup, we can see slight differences. The first list has two sets of HTML-Comments to act as placeholders for the two ngSwitchCase
directives. And, the second list only has one HTML-Comment to act as a placeholder for the one ngTemplateOutlet
directive. This difference is immaterial, really; I'm simply pointing it out to show that the two lists have a slightly different runtime implementation.
Now that I see this on paper, I am not sure how I feel. With such a trivial demo, the first option is clear and concise - the second option feels overly verbose. However, I wonder if this balance would change as the list rendering becomes more complex? The nice thing about the second option is that it separates-out the list rendering from the item rendering. This could become a net-benefit in more complex cases? But, I am not entirely sold.
It's fun to see how dynamic Angular's component templates can be. The platform truly offers us a tremendous amount of flexibility to find the right solution for the right problem. For now, when it comes to rendering a list of mixed data-types, I'll probably stick to my traditional ngSwitch
technique. However, as my HTML markup becomes more complex, I will definitely continue to experiment with the ngTemplateOutlet
approach to see if it offers any additional clarity.
Want to use code from this post? Check out the license.
Reader Comments
I actually think the first example might fair better as the complexity increases, because I could imagine the expression inside:
Becoming somewhat difficult to understand.
@Charles,
In terms of "complexity", I was thinking more about the complexity of the item-rendering itself (like, how much markup it takes to render each given item type). But, I think you're probably right. A simple
[ngSwitchCase]
for each item type is going to be easier to read.I guess I just needed to see it on paper before I could narrow in on how it would feel.
Having said that. It's great to have a range of options available. So it is a valid exploration!
@Ben , How do you format your code with the ( extra spaces inside ) ?
@NSN,
I am not sure what you mean by "how"? Are you talking about how I write my actual code? Or how it renders on the blog?
@Ben,
In your code, also in YouTube videos, the format of the text is adding more spaces after
(
and before)
so (this) looks like ( this ) and it is much much better for the eye to read... but, I didnt find a way to do this with a formatter like Prettier.Thanks man !
@NSN,
That's just how I write my code :D I don't have a linter or anything. Just muscle-memory :D