Rendering A List Of Mixed Components Using NgFor And NgSwitch In Angular 7.2.13
In the vast majority of cases, when I use the NgFor directive to render a collection of data, the type of data contained within that list is uniform. However, sometimes, a single list is an aggregation of several different collections that result in a commingled set of data types. In such a case, we can still render the list using a normal NgFor loop; however, within the NgFor template, we need to use a nested directive in order to render the appropriate Component (or template) based on some sort of type-differentiator. I tend to use the NgSwitch and NgSwitchCase directives in Angular 7.2.13 as I think they lend well to a clean, readable syntax; though, you could also use several NgIf directives as well.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
To see this in action, I'm going to create a super simple App component that attempts to render a list of commingled data types: "a" and "b". Each of these types will map to a unique component, ThingAComponent and ThingBComponent respectively:
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface Thing {
type: "a" | "b";
value: string;
}
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
templateUrl: "./app.component.htm"
})
export class AppComponent {
public things: Thing[];
// I initialize the app component.
constructor() {
// When dealing with a mixed-list of data, we need to have some sort of
// differentiator ("type" in this case) so that we can figure out which item
// maps to which class of component in the View.
this.things = [
{ type: "a", value: "A1" },
{ type: "a", value: "A2" },
{ type: "b", value: "B1" },
{ type: "a", value: "A3" },
{ type: "b", value: "B2" }
];
}
// ---
// PUBLIC METHODS.
// ---
// I log the clicked thing.
public handlClick( thing: Thing ) : void {
console.group( "You clicked a thing!" );
console.log( thing );
console.groupEnd();
}
}
As you can see, we have a single collection, "things", which contains objects with a differentiator attribute, "type". Now, let's look at how to render this single list while mapping the unique types to the appropriate components:
<!--
When rendering a list that contains components, it's always helpful to separate out
the "layout concerns" from the "content concerns". Meaning, UL and LI elements here
don't concern themselves with the list content - only with the rendering of the list
infrastructure.
-->
<ul class="items">
<li
*ngFor="let thing of things"
class="items__item"
[ngSwitch]="thing.type">
<!--
Inside of the list-item, we can now focus on the items that we are actually
displaying. And to cope with a "mixed list", let's use the ngSwitch directive
(above) and the ngSwitchCase directive (below) to conditionally render the
appropriate component based on the item Type.
--
NOTE: We could have put the *ngSwitchCase directly on the Thing Components;
but, using an ng-template helps to articulate the mutually-exclusive nature
of the components within the list context.
-->
<ng-template ngSwitchCase="a">
<my-thing-a
[value]="thing.value"
(click)="handlClick( thing )">
</my-thing-a>
</ng-template>
<ng-template ngSwitchCase="b">
<my-thing-b
[value]="thing.value"
(click)="handlClick( thing )">
</my-thing-b>
</ng-template>
</li>
</ul>
Within this template, we have two responsibilities: rendering the list layout; and, rendering the appropriate component within each list item. When we think about these facets as two completely separate concerns, it makes it easier to see where we should place our directives.
Rendering the list is simple, we just use the NgFor directive like we would anywhere else. In other words, the NgFor directive doesn't actually care that the data it's rendering represents a commingled set of data-types. It only knows that it has a single collection and a single rendering template.
The responsibility of mapping Types to Components is wholly owned by the NgFor template. And, to do this, we're going to use the NgSwitch directive on the LI element in order to setup our differentiator (type). Then, within the differentiator, we're going to use the NgTemplate and NgSwitchCase directives to conditionally render a single Component within each LI instance.
Since there are multiple ways to consume structural directives in an Angular template, we could have just as easily used the "*ngSwitchCase" syntactic sugar directly on each Component. However, this would make it harder to read as the components would look, at a glance, like siblings. What I like about the NgTemplate approach is that it creates clearer delineation between the set of possible outputs, showcasing the fact that the rendering of any one Component is a mutually-exclusive state within a single LI.
Now, if we run this Angular application and render the collection of mixed data types in the browser, we get the following output:
As you can see, we were able to use a single NgFor directive to render a collection of mixed components in Angular.
At first, it may be tempting to try and use NgComponentOutlet to render a list of commingled data-types. But, at least at the time of this writing, the NgComponentOutlet directive doesn't allow you to bind to Inputs or Outputs of the rendered component. As such, that's a deal-breaker in almost all rendering situations that I've come across. By using a switch with actual components, we can fully bind to all aspects of the individual components, even allowing for different bindings for each component type.
Rendering a list of mixed data types may seem daunting at first in Angular 7.2.13. But, when you separate the concerns of "layout" from the concerns of "content", the technique naturally presents itself. We continue to use NgFor to render the "layout", just as we've always done; and then, we introduce the NgSwitch and NgSwitchCase directives in order to render to various types of "content".
Want to use code from this post? Check out the license.
Reader Comments
Ben. This really threw me, until I looked at your GitHub repo. I was asking myself, where did the values displayed in the screenshot, come from? In other words:
Where did the capitalisation and curved brackets come from?
OK. So then I thought everything would become clear, but I still cannot see where you are referencing the 'ThingAComponent' & 'ThingBComponent' in:
I was expecting to see something like:
In:
I then looked in your:
Although I can see that you have imported the components here, I was always under the impression, that components need to be explicitly imported into the parent component as well? In this case that would be:
If this is not the case, I have an awful lot of needless 'imports' in my Angular projects!
@Charles,
Ah, sorry about that! I usually show the peripheral components. I think I was running short on time, so I had to speed through this write-up a bit. As far as importing Directives, with the
NgModule
, you only need to add them to thedeclarations
property of the parentNgModule
- that will make them automatically available to the other directives in the same module.Before we had
NgModel
, you had toimport
your Directives and then add them to the@Component()
meta-data. But, once we stopped using the meta-data, you no longer have to import them. From the documentation:Hope that helps a bit. And sorry for leaving so much of this post out of view.
OK. I get it now. I completely missed the selector part. I'm so used to prefixing my component selectors with:
That's what I really like about your code. I have to look through it, at least 3 times, before everything comes together!
this is the best article ive read this year. Serious gold
Highly appreciated content STRONGLY RECOMMENDED...!!!
@S,
Thank you very much :)
Thanks!
This is a pattern I've been using as well.
Looking for an elegant solution to the related problem of referencing disparate types from the template; For example, a list of apples and pears and I need to bind different properties on each using a member variable like currentFruit. In the TS the discriminated union works well, but I get lint warnings in the component without doing hokey workarounds.
Currently, in order to avoid calling a function from the template which casts currentFruit, we are keeping a separate var (currApple, currPear, etc) around in the component so that each ngSwitch has a variable of the correct subtype to use.
(we are using Angular Language Service extension in VS Code)
@Joseph,
Yeah, I think I ran into this same issue as well - it seems that Discriminated Unions don't work well in the template:
www.bennadel.com/blog/3696-discriminated-unions-don-t-seem-to-work-in-angular-9-0-0-next-5-when-fulltemplatetypecheck-is-enabled.htm
I "worked around" this issue by disabling
fullTemplateTypeCheck
in mytsconfig.json
. I know that's not a "solution"; but, I still haven't found a way to get around that. This still happens in9.0.0-rc.4
- I actually ran into this again just yesterday. And, again, I had to disable the template checks.Of course, it sucks to have to disable this. I really hope someone else can suggest a better fix.
@Joseph,
The best bet might be to create separate components for each Type, and then just pass the list-item into the component as an input bindings. This way, you can sort of push the type differentiation down into the various component implementations. I think Angular would be OK with this, since you could "type" the input binding within the components.
But, this creates a lot more work if all you wanted was some simple Template markup :(
@Ben,
Good thoughts. Thanks!
@All,
After playing around with a lot of
ng-template
stuff lately, I got the inspiration to try and render a list of mixed data-types usingngTemplateOutlet
:www.bennadel.com/blog/3738-rendering-a-list-of-mixed-types-using-ngfor-and-ngtemplateoutlet-in-angular-9-0-0-rc-5.htm
After seeing it on paper, it definitely doesn't make sense for "simple" use-cases. However, I'm not sure how it will look for more complicated HTML. It might be worth experimenting with in the future.