Angular Content Projection
Hey everyone! It’s been a while! I’m back with a fresh topic: Angular Content Projection. Despite its apparent simplicity, even developers at beginner to intermediate levels might find themselves scratching their heads over it. In fact, many of us have probably used content projection without realizing it. Let’s break it down together!
So, the million-dollar question: what is content projection? In Angular, content projection is a powerful technique that allows you to insert custom HTML content into a component from another component. It’s like having a container component that accepts different pieces of UI from other components, giving you more flexibility and reusability in your application.
To go through the concepts as usual I have created a simple application. Here I have two components, one is the root component which is our app component, and the other is a product card component. So, our goal is to make a reusable product card which can adapt to different content, reducing code duplication.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { ProductCardComponent } from './product-card/product-card.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, ProductCardComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'content-projection-demo';
onButtonClick() {
console.log('Button clicked');
}
}
<div class="product-container">
<app-product-card (isAddToCartClicked)="onButtonClick()"> </app-product-card>
<app-product-card (isAddToCartClicked)="onButtonClick()"> </app-product-card>
</div>
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CommonModule],
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss']
})
export class ProductCardComponent {
@Output() isAddToCartClicked = new EventEmitter();
addToCart(){
this.isAddToCartClicked.emit(true);
}
}
<div class="product-card">
<img
class="blog_posts_post--image"
[src]="'../assets/22.jpg'"
loading="lazy"
/>
<div class="price-label">
<p class="blog_posts_post--date">$100</p>
</div>
<p class="product-name">Mountains Bike</p>
<p class="product-description"> Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s
</p>
<div class="add-to-cart" (click)="addToCart()">
<a>Add To Cart</a>
</div>
</div>
Imagine that we need to change the item name of the card component. We require two product cards: one for the Mountain Bike and the other for the Standard. Let’s explore how we can achieve this using content projection.
<!-- app component -->
<div class="product-container">
<app-product-card (isAddToCartClicked)="onButtonClick()">
<h1>Mountain Bike</h1>
</app-product-card>
<app-product-card (isAddToCartClicked)="onButtonClick()">
<h1>standard Bike</h1>
</app-product-card>
</div>
<!-- product card component -->
<div class="price-label">
<p class="blog_posts_post--date">$100</p>
</div>
<ng-content></ng-content>
<p class="product-description">
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s
</p>
What we’ve done is removed the hardcoded heading from the product card component and introduced a new syntax, <ng-content></ng-content>, which acts as a placeholder for displaying content. Then, from the parent component, we pass the content (heading) between the components.
Seems pretty simple, right? You might wonder why we’re using ng-content instead of simply passing it as an input for the card component. Well, this is just a straightforward example that demonstrates a glimpse of how powerful ng-content is. Unlike inputs, we can pass entire contents (components, complex templates) using these content projection mechanisms. We’ll explore them one by one.
Other than the product card heading, let’s consider making the Add to Cart button dynamic as well. There might be a scenario where we want to change it to a Pre-Order button instead. If we simply pass the button along with the heading, we’ll notice that the button gets projected below the heading. But that’s not where we want it, right? The reason for this is that we’ve only provided a single projection slot in the product component, so whatever content we pass gets displayed within that single <ng-content> slot.
To resolve this, we can utilize Multi-Slot Content Projection. This is incredibly useful because it allows us to project multiple directives into the component and DOM. In our case, we can add another <ng-content> slot for the button in the card component. But how does Angular know which content should be shown in which slot? It’s simple: we can add a select attribute to the <ng-content> elements. Angular supports selectors for any combination of tag name, attribute, CSS class, or ID. Based on these selectors, Angular identifies the correct content that should be projected into each slot.
<!-- app component -->
<div class="price-label">
<p class="blog_posts_post--date">$100</p>
</div>
<ng-content select=".product-name"></ng-content>
<p class="product-description">
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s
</p>
<ng-content select=".add-to-cart"></ng-content>
<!-- product card component -->
<div class="product-container">
<app-product-card (isAddToCartClicked)="onButtonClick()">
<p class="product-name">Mountain Bike</p>
<div class="add-to-cart" (click)="onButtonClick()">
<a>Add To Cart</a>
</div>
</app-product-card>
<app-product-card (isAddToCartClicked)="onButtonClick()">
<p class="product-name">Standard Bike</p>
<div class="add-to-cart" (click)="onButtonClick()">
<a>Add To WishList</a>
</div>
</app-product-card>
</div>
Now that we understand that we can pass any kind of content to a projection slot and project it, let’s consider if we have a component, maybe a form. How can we pass a whole component and project it while binding values for that component? Let’s take a look at the same example and slightly modify it.
import { Component, EventEmitter, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-notify-me',
standalone: true,
imports: [CommonModule, MatCheckboxModule, FormsModule],
template: `
<mat-checkbox class="example-margin" (change)="onChecked($event.checked)"
>Notify Me when Stock Arrives</mat-checkbox
>
`,
styleUrls: ['./notify-me.component.scss'],
})
export class NotifyMeComponent {
@Output() checked = new EventEmitter();
onChecked($event: boolean) {
this.checked.emit($event);
}
}
<div class="product-container">
<app-product-card (isAddToCartClicked)="onButtonClick()">
<h1>Mountain Bike</h1>
</app-product-card>
<app-product-card (isAddToCartClicked)="onButtonClick()">
<h1>standard Bike</h1>
<app-notify-me (checked)="onNotifyMeClicked($event)"></app-notify-me>
</app-product-card>
</div>
So here I’ve created a new Angular component named app-notify-me. This component serves as a checkbox that triggers the onChecked callback function upon being clicked, emitting a boolean value. In the parent component, app-component, I’ve seamlessly integrated the app-notify-me component, positioning it between app-product-card elements and establishing a binding to its checked output function. We have created another ng-content projection slot with notify me component name as our selector. It’s projecting this newly created notify me component. Here’s a snippet of how it all comes together:
<div class="button-checkbox-container">
<div class="add-to-cart" (click)="addToCart()">
<a>Add To Cart</a>
</div>
<ng-content select="app-notify-me"></ng-content>
</div>
I got to say, playing around with content projection in Angular has been a blast. It’s like this superpower that lets you build components that are efficient and reusable. Sure, our examples have been pretty basic, but trust me, this stuff can handle way more complex content arrangements too. Just thought I’d share the excitement with you! Angular’s got some neat tricks up its sleeve, and We have just getting started exploring them. Catch you later with an awesome post like this 😉, Till then, happy coding!