Use ng-content (content projection), ng-template, ngTemplateOutlet and ng-container for a reusable component in a real-world use case with nice example code

In this blog, I will define a use case where it makes sense to employ content projection using ng-content as well as ng-template with ngTemplateOutlet and ng-container. I will explain what these directives are and why we are using them.

As usual, you can find a fully working project on my GitHub

Angular Version: 9.1



Our requirements (use case)

As part of a web application for RV users, we have been given these requirements to create a reusable card component that will assure a consistent look-and-feel and branding for cards used throughout the application:

  • Card must use our logo icon

  • All cards must have the title “RV Like Me” with a consistent look and feel that is not configurable by the user of the reusable component.
  • For phase one, the component should accept a card type of ‘lifestyle’ or ‘rig’ and this should be displayed nicely as a subtitle.
  • The next area of the card should be an area for a picture, or multiple pictures, or really anything the user of the reusable component wants to put there.
  • Next, should be a text area the user of the reusable component could fill with something like location or other meta information. For flexibility, the may configure the content and look-and-feel of this area.
  • The next area should be a text area that the user of the reusable component may fill with something like a description. For flexibility, they may configure the content and look-and-feel of this area.
  • There are two buttons under the description, LIKE and SHARE that are not configurable and should always have the same look and location on the card.

Here is an example of a card:

What is content projection and how can it help with our requirements?

Based on these requirements, it makes sense to pass a reference to the card type through an @Input decorator. But the rest of the requirements give much flexibility to the user of our component, which would make it difficult to use @Input. Instead we will allow the user to ‘project’ their HTML content into our component by using the ng-content directive!

The ng-content directive can be used as a placeholder in the target component to ‘project’ HTML from the parent component that is using the target component’s selector in it’s HTML. This makes it ideal for our requirements, where we want the user of our reusable component to be able to provide styled HTML content to our component. Let’s see how it works.

I created a component called content-projection and added Angular Material so we can use Material’s <mat-card> as our host container, as it provides the structure needed to support the requirement. In case you haven’t done this, here are the CLI commands.

ng new content-projection
ng install @angular/material @angular/cdk @angular/animations
ng add @angular/material

I also created a component called ‘card’ in our content-projection directory like this:

ng g c card

card will be our reusable card component. in card.component.html I added the following:

HTML

<mat-card>

  <mat-card-header>
    <mat-card-title>RV Like Me</mat-card-title>
    <mat-card-subtitle></mat-card-subtitle>
    <img mat-card-avatar src="./../../assets/images/logo.png">
  </mat-card-header>

  <ng-content select="[image]"></ng-content>

  <mat-card-content>
    <ng-content select="[location]"></ng-content>
    <ng-content select="[description]"></ng-content>
  </mat-card-content>

  <mat-card-actions>
    <button mat-button>LIKE</button>
    <button mat-button>SHARE</button>
  </mat-card-actions>

</mat-card>

CSS

mat-card {
  margin: 5px;
  max-width: 300px;
}

mat-card-header {
  margin-bottom: 10px;
}

mat-card-content {
  margin-top: 10px;
}

.main-image{
  width: 100%;
}

Typescript

import { Component, OnInit, Input } from '@angular/core';

enum CardType {
  lifestyle = 'Lifestyle',
  rig = 'Rig'
}

@Component({
  selector: 'app-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit {
  @Input() cardType: string;

  type: string;

  constructor() { }

  ngOnInit(): void {
    this.type = CardType[this.cardType];
  }

}

In <mat-card-header>, we use the title and logo provided in our requirements. We reference the cardType from the parent through the @Input decorator and apply it to an enum for consistency, displaying in the template in <mat-card-subtitle>. <mat-actions> implements the consistent ‘LIKE’ and ‘SHARE’ buttons.

Under <mat-card-header> we put the image using <ng-content select="[image]"></ng-content>. Note the use of the select property which references a variable called image in the parent component. This allows the parent component to specify the HTML to be projected into this specific area of the card. We use the same technique in <mat-card-content> to project the location and description HTML into the appropriate place in our card.

Let’s see how we would use our component. In app.component.html I added this:

<div>
    <app-card
      [cardType]="'lifestyle'">
      <img image style="width:100%" src="./../../assets/images/mccall-sunset.jpg">
      <p location>McCall, ID</p>
      <p description>This was the view from our RV park.  Beautiful here!</p>
    </app-card>
</div>

We pass the card type of ‘lifestyle’ through property binding. We use the image reference variable on the <img> tag to identify where we want to put the image in the card, as well as the location and description reference variables.

We try it out and it looks like the example in our requirements!

Now let’s make sure it has the flexibility we desire in our reusable component.

<div>
  <app-card
    [cardType]="'lifestyle'">
    <div image style="text-align:center">
      <a style="text-decoration:none; cursor:pointer;"
          href="./../../assets/images/mccall-sunset.jpg" target="_blank">
          <img style="width:32.3%; margin-right:1.03%" src="./../../assets/images/mccall-sunset.jpg">
      </a>

      <a style="text-decoration:none; cursor:pointer;"
          href="./../../assets/images/mccall-lake.jpg" target="_blank">
          <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-lake.jpg">
      </a>

      <a style="text-decoration:none; cursor:pointer;"
          href="./../../assets/images/mccall-snow.jpg" target="_blank">
          <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-snow.jpg">
      </a>
    </div>
    <p location>
      Plan your visit to
      <a href="https://visitmccall.org" class="hyperlink" target="_blank">McCall, ID</a>
      !
    </p>
    <div description>
      <p>Things to see in McCall, ID</p>
      <ul>
        <li>Payette Lake</li>
        <li>Ponderosa State Park</li>
        <li>Central Idaho Historical Museum</li>
      </ul>
    </div>
  </app-card>
</div>

Instead of a single image, we’re passing in 3 smaller images that are clickable to enlarge. The text for location contains an external link and the description is now a list.

One important thing to point out is that the styles are coming from the parent. I’ve used both in-line styles and a style class in the app.component.

Let’s see what this looks like now:

Great! We’ve given the user the flexibility to change the content as they see fit, while maintaining consistancy and branding!

Now, how can we make this code more efficient? ng-template and ng-container!

ng-template

ng-template allows you to wrap some HTML that will not be included in the DOM unless you apply a structural directive like *ngIf.

This code will show nothing. It is just holding the template:

<ng-template>
  <p>the text here will not be seen in the DOM</p>
</ng-template>

But this template will be seen in the DOM provided showText evaluates to true:

<ng-template *ngIf="showText">
  <p>the text here will not be seen in the DOM</p>
</ng-template>

ng-template itself will not show in the DOM but if you had used a div instead, the div would show. So, it helps you optimize your HTML.

I will use ng-template in our code to hold the ‘single image’ and ‘multi-image’ examples we saw above and alternate them based on a random number generator. First, we will use div and then we will try ng-template and see the difference in the DOM.

HTML

<div [ngSwitch]="cardType">

  <div *ngSwitchCase="'single'">
      <app-card
        [cardType]="'lifestyle'">
        <img image style="width:100%" src="./../../assets/images/mccall-sunset.jpg">
        <p location>McCall, ID</p>
        <p description>This was the view from our RV park.  Beautiful here!</p>
      </app-card>
  </div>

  <div #multi>
    <app-card
      [cardType]="'lifestyle'">
      <div image style="text-align:center">
        <a style="text-decoration:none; cursor:pointer;"
            href="./../../assets/images/mccall-sunset.jpg" target="_blank">
            <img style="width:32.3%; margin-right:1.03%" src="./../../assets/images/mccall-sunset.jpg">
        </a>

        <a style="text-decoration:none; cursor:pointer;"
            href="./../../assets/images/mccall-lake.jpg" target="_blank">
            <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-lake.jpg">
        </a>

        <a style="text-decoration:none; cursor:pointer;"
            href="./../../assets/images/mccall-snow.jpg" target="_blank">
            <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-snow.jpg">
        </a>
      </div>
      <p location>
        Plan your visit to
        <a href="https://visitmccall.org" class="hyperlink" target="_blank">McCall, ID</a>
        !
      </p>
      <div description>
        <p>Things to see in McCall, ID</p>
        <ul>
          <li>Payette Lake</li>
          <li>Ponderosa State Park</li>
          <li>Central Idaho Historical Museum</li>
        </ul>
      </div>
    </app-card>
  </div>

  <div *ngSwitchDefault>
    <p>I see nothing</p>
  </div>

</div>

Typescript

enum CardType {
  'single' = 1,
  'multi' = 2
}

cardType: string;

ngOnInit(): void {
  const cardNbr = Math.floor(Math.random() * 2) + 1;
  this.cardType = CardType[cardNbr];
}

What does this look like in the DOM? Using Chrome’s development tools we can inspect the source code and see all of the divs we are using:

Now, we will try the ng-template directive, replacing the divs around our two <app-card> selectors:

<div [ngSwitch]="cardType">

  <ng-template [ngSwitchCase]="'single'">
      <app-card
        [cardType]="'lifestyle'">
        <img image style="width:100%" src="./../../assets/images/mccall-sunset.jpg">
        <p location>McCall, ID</p>
        <p description>This was the view from our RV park.  Beautiful here!</p>
      </app-card>
  </ng-template>

  <ng-template [ngSwitchCase]="'multi'">
    <app-card
      [cardType]="'lifestyle'">
      <div image style="text-align:center">
        <a style="text-decoration:none; cursor:pointer;"
            href="./../../assets/images/mccall-sunset.jpg" target="_blank">
            <img style="width:32.3%; margin-right:1.03%" src="./../../assets/images/mccall-sunset.jpg">
        </a>

        <a style="text-decoration:none; cursor:pointer;"
            href="./../../assets/images/mccall-lake.jpg" target="_blank">
            <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-lake.jpg">
        </a>

        <a style="text-decoration:none; cursor:pointer;"
            href="./../../assets/images/mccall-snow.jpg" target="_blank">
            <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-snow.jpg">
        </a>
      </div>
      <p location>
        Plan your visit to
        <a href="https://visitmccall.org" class="hyperlink" target="_blank">McCall, ID</a>
        !
      </p>
      <div description>
        <p>Things to see in McCall, ID</p>
        <ul>
          <li>Payette Lake</li>
          <li>Ponderosa State Park</li>
          <li>Central Idaho Historical Museum</li>
        </ul>
      </div>
    </app-card>
  </ng-template>

  <ng-template *ngSwitchDefault>
    <p>I see nothing</p>
  </ng-template>

</div>

Now let’s look at the DOM again. Two of the divs are gone. The only one remaining is the wrapper for the parent component.

ng-container and ng-templateOutlet

We can also avoid another unnecessary div, which is holding our ngSwitch, by replacing with ng-container.

The ng-container directive provides us with an element that we can attach a structural directive to without having to create an extra element in the DOM.

We could simplify further using ngTemplateOutlet to reference the appropriate templates. This makes for cleaner code.

<ng-container [ngSwitch]="cardType">

  <ng-container *ngSwitchCase="'single'">
    <ng-container *ngTemplateOutlet="single"></ng-container>
  </ng-container>

  <ng-container *ngSwitchCase="'multi'">
    <ng-container *ngTemplateOutlet="multi"></ng-container>
  </ng-container>

  <ng-container *ngSwitchDefault>
    <ng-container *ngTemplateOutlet="default"></ng-container>
  </ng-container>

</ng-container>

<ng-template #single>
  <app-card
    [cardType]="'lifestyle'">
    <img image style="width:100%" src="./../../assets/images/mccall-sunset.jpg">
    <p location>McCall, ID</p>
    <p description>This was the view from our RV park.  Beautiful here!</p>
  </app-card>
</ng-template>

<ng-template #multi>
  <app-card
    [cardType]="'lifestyle'">
    <div image style="text-align:center">
      <a style="text-decoration:none; cursor:pointer;"
          href="./../../assets/images/mccall-sunset.jpg" target="_blank">
          <img style="width:32.3%; margin-right:1.03%" src="./../../assets/images/mccall-sunset.jpg">
      </a>

      <a style="text-decoration:none; cursor:pointer;"
          href="./../../assets/images/mccall-lake.jpg" target="_blank">
          <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-lake.jpg">
      </a>

      <a style="text-decoration:none; cursor:pointer;"
          href="./../../assets/images/mccall-snow.jpg" target="_blank">
          <img style="width:32.3%; margin-left:1.03%" src="./../../assets/images/mccall-snow.jpg">
      </a>
    </div>
    <p location>
      Plan your visit to
      <a href="https://visitmccall.org" class="hyperlink" target="_blank">McCall, ID</a>
      !
    </p>
    <div description>
      <p>Things to see in McCall, ID</p>
      <ul>
        <li>Payette Lake</li>
        <li>Ponderosa State Park</li>
        <li>Central Idaho Historical Museum</li>
      </ul>
    </div>
  </app-card>
</ng-template>

We can see here that there is no div clutter at all. Just our container to host the child component!

On a side note, if you want to apply two structural directives to an element, you could wrap the element in ng-container and apply one there, as Angular does not allow two structural directives on one element.

Conclusion

Hopefully, by using a real-world use case, you can see how to use ng-content for projection of HTML from a parent template to a child template and how to use ng-template with ngTemplateOutlet to store and switch between HTML and ng-container to host structural directives without DOM impact.

As a reminder, you can find a fully working project on my GitHub.

Feel free to contact me at dave@dev-reboot.com if you have any questions or comments.

Keep on developing!

Written on July 19, 2020