Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Angular 2 Master Class (New York, NY) with: Paul Barrick and Alina Barrick
Ben Nadel at Angular 2 Master Class (New York, NY) with: Paul Barrick@peb7268 ) and Alina Barrick

Rendering A List Of Mixed Components Using NgFor And NgSwitch In Angular 7.2.13

By Ben Nadel on

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:


 
 
 

 
 Using a single NgFor directive to render mixed data lists in Angular 7.2.13. 
 
 
 

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".



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:


Thing A (A1)

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:

app.component.ts

I was expecting to see something like:

import { ThingAComponent } from "./thing-a.component";	
import { ThingBComponent } from "./thing-b.component"

In:

app.component.ts

I then looked in your:

app.module.ts

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:

app.component.ts

If this is not the case, I have an awful lot of needless 'imports' in my Angular projects!

Reply to this Comment

@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 the declarations property of the parent NgModule - that will make them automatically available to the other directives in the same module.

Before we had NgModel, you had to import 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:

A component must belong to an NgModule in order for it to be available to another component or application. To make it a member of an NgModule, list it in the declarations field of the NgModule metadata.

Hope that helps a bit. And sorry for leaving so much of this post out of view.

Reply to this Comment

OK. I get it now. I completely missed the selector part. I'm so used to prefixing my component selectors with:

app-

That's what I really like about your code. I have to look through it, at least 3 times, before everything comes together!

Reply to this Comment

this is the best article ive read this year. Serious gold
Highly appreciated content STRONGLY RECOMMENDED...!!!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.