Content Projection Fallback in ng-content in Angular

Connie Leung - Jul 2 - - Dev Community

Introduction

In this blog post, I want to describe a new Angular 18 feature called content projection fallback in ng-content. When content exists between the ng-content opening and closing tags, it becomes the fallback value. When projection does not occur, the fallback value is displayed.

Bootstrap Application

// app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection()
  ]
};
Enter fullscreen mode Exit fullscreen mode
// main.ts

import { appConfig } from './app.config';

bootstrapApplication(App, appConfig);
Enter fullscreen mode Exit fullscreen mode

Bootstrap the component and the application configuration to start the Angular application.

Create a Card Component

The AppCard component displays the default tier and its default features. Then, an AppPricingListComponent encapsulates the AppCardComponent and passes the custom tier and features to it to display. Finally, the App component consists of AppCardComponent and AppPricingListComponent to build the full pricing page.

// app-card.component.ts

import { ChangeDetectionStrategy, Component } from "@angular/core";

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="header">
      <ng-content select="[header]">Free Tier</ng-content>
    </div>
    <div class="content">
      <ng-content> 
        <ul>
          <li>Free of Charge</li>
          <li>1 License</li>
          <li>500MBs Storage</li>
          <li>No Support</li>
        </ul> 
      </ng-content>
    </div>
    <div class="footer">
      <ng-content select="[footer]">
        <button>Upgrade</button>
      </ng-content>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppCardComponent {}
Enter fullscreen mode Exit fullscreen mode

The template of the AppCardComponent has three sections: header, content, and footer. The header section consists of a ng-content that projects to an element with a header attribute. When the projection does not occur, the fallback value, "Free Tier", is displayed. The content section comprises a ng-content element that projects to the default element. This section renders the free tier's features when the projection does not occur. The footer section comprises a ng-content element that projects to an element with a footer attribute. When the projection does not occur, the upgrade button is rendered.

Create a Price Listing Component

This is a simple component that encapsulates AppCardComponent to display the signal input values of a tier and its features.

// app-price-list.component.ts

import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { AppCardComponent } from "./app-card.component";

@Component({
  selector: 'app-price-card',
  standalone: true,
  imports: [AppCardComponent],
  template: `
    <section>
      <h2>Custom Content</h2>
      <app-card>
        <div header>{{ tier() }}</div>
        <ul>
          @for (item of features(); track item) {
            <li>{{ item }}</li>
          }
        </ul>
      </app-card>
    </section>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppPricingListComponent {
  tier = input<string>('');
  features = input<string[]>([]);
}
Enter fullscreen mode Exit fullscreen mode

Build a full pricing page using content projection fallback

// main.ts

import { Component, VERSION } from '@angular/core';
import { AppCardComponent } from './app-card.component';
import { AppPricingListComponent } from './app-price-list.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AppCardComponent, AppPricingListComponent],
  template: `
    <header>Angular {{version}} - Content Projection fallback </header>

    <main>
      <section>
        <h2>Fallback content</h2>
        <app-card />
      </section>

      <app-price-list tier="Start-up" [features]="startUpFeatures" />
      <app-price-list tier="Company" [features]="companyFeatures" />
      <section>
        <h2>Custom Content</h2>
        <app-card>
          <div header>Enterprise</div>
          <ul>
            <li>Contact sales for quotation</li>
            <li>200+ Licenses</li>
            <li>1TB Storage</li>
            <li>Email and Phone Technical Support</li>
            <li>99.99% Uptime</li>
          </ul>
          <div footer>&nbsp;</div>
        </app-card>
      </section>
    </main>
   `,
})
export class App {
  version = VERSION.full;

  startUpFeatures = [
    'USD 10/month',
    '3 Licenses',
    '1GB Storage',
    'Email Technical Support',
  ];

  companyFeatures = [
    'USD 100/month',
    '50 Licenses',
    '20GB Storage',
    'Email and Phone Technical Support',
    '95% Uptime'
  ];
}
Enter fullscreen mode Exit fullscreen mode

The pricing page has four cards: the free tier card displays the fallback values because the AppCardComponent does not have a body. The startup and company cards use the footer's fallback value and project the header and features. Therefore, both cards still display the upgrade button. Similarly, the enterprise card projects the header and features. It also projects the footer to replace the upgrade button with a blank row.

The following Stackblitz repo displays the final results:

This is the end of the blog post that describes content projection fallback with ng-content in Angular 18. I hope you like the content and continue to follow my learning experience in Angular, NestJS, GenerativeAI, and other technologies.

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .