Angular Content Projection: A Comprehensive Guide
Content projection is a powerful feature in Angular that allows you to create more flexible and reusable components.
What is content projection?
At its core, content projection is a pattern that enables a component to receive and display content from its parent component. This content can be simple text, HTML elements, or even other Angular components. The receiving component acts as a shell or a template, defining where the projected content should be placed.
Why is it important in Angular?
Content projection is crucial in Angular for several reasons:
Reusability: it allows you to create more generic, reusable components that can adapt to different contexts.
Flexibility: parent components have more control over the content of their child components.
Separation of Concerns: it helps maintain a clear separation between the structure (provided by the parent) and the presentation (handled by the child).
Dynamic UIs: it enables the creation of dynamic and adaptable user interfaces.
Brief history and evolution in Angular
Content projection has been a part of Angular since its early versions, evolving from the concept of transclusion in AngularJS (Angular 1.x). In modern Angular:
Angular 2+ introduced the
<ng-content>
element, replacing the oldng-transclude
directive.Subsequent versions have added features like multi-slot projection and more powerful querying of projected content.
Single-slot content projection
The simplest form of content projection involves a single projection slot. This is achieved using the <ng-content>
element in the child component's template.
The ng-content
element
The <ng-content>
element is a placeholder for the content that will be projected from the parent component. When Angular compiles the component, it replaces <ng-content>
with the content provided by the parent.
How to use it in your components
Let's look at a basic example:
Create a child component, let's call it
CardComponent
:
// card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<h2>Card Title</h2>
<ng-content></ng-content>
</div>
`
})
export class CardComponent {}
Use the
CardComponent
in a parent component:
// parent.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<app-card>
<p>This content will be projected into the card.</p>
</app-card>
`
})
export class ParentComponent {}
In this example, the <p>
element and its content from the parent component will be projected into the CardComponent
, replacing the <ng-content>
element.
When rendered, the output will look like this:
<div class="card">
<h2>Card Title</h2>
<p>This content will be projected into the card.</p>
</div>
This basic example demonstrates how content projection allows you to create a reusable card component that can accept and display different content from parent components.
Multi-slot Content Projection
Sometimes, you may want to project content into multiple specific areas of your component. Angular supports this through multi-slot content projection.
Using multiple ng-content
elements
To implement multi-slot projection, you use multiple <ng-content>
elements in your component template, each with a unique select
attribute.
The select
attribute
The select
attribute on <ng-content>
allows you to define where specific content should be projected. You can select content based on:
Element names
CSS classes
Attributes
Projecting content into specific slots
Let's enhance our CardComponent
to support multi-slot projection:
// card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<header>
<ng-content select="[card-header]"></ng-content>
</header>
<div class="card-body">
<ng-content></ng-content>
</div>
<footer>
<ng-content select="[card-footer]"></ng-content>
</footer>
</div>
`
})
export class CardComponent {}
Now, let's use this enhanced CardComponent
in a parent component:
// parent.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<app-card>
<h2 card-header>Card Title</h2>
<p>This is the main content of the card.</p>
<button card-footer>Action</button>
</app-card>
`
})
export class ParentComponent {}
In this example:
Content with the
card-header
attribute will be projected into the header.Content with the
card-footer
attribute will be projected into the footer.Any other content will be projected into the main body of the card.
This multi-slot projection allows for more complex and flexible component structures, enabling you to create highly reusable components that can adapt to various use cases.
Conditional Content Projection
Handling cases when no content is projected
You can use @if
to provide fallback content when nothing is projected (we don’t have to do this in Angular 18 anymore):
@Component({
selector: 'app-fallback-content',
template: `
<ng-content select="[header]"></ng-content>
@if (!hasHeaderContent()) {
<h2>Default Header</h2>
}
`
})
export class FallbackContentComponent {
headerContent = contentChild<ElementRef>('header');
}
Note that in this example we use signal queries so it does not matter if we use the component lifecycle (afterContentInit
) or not because it’s a reactive value (when the value arrives, the binding changes). This is insane!
Fallback Content in ng-content 🔥 (NEW Angular 18)
Instead of playing with signal queries
and @if
you can now use the fallback content that comes with Angular 18.
@Component({
selector: 'card-component',
standalone: true,
template: `
<ng-content select="header">Default header</ng-content>
<ng-content>Default card content</ng-content>
<ng-content select="footer">Default footer</ng-content>
`
})
export class CardComponent {}
And if we use it in another component:
@Component({
selector: 'other',
standalone: true,
template: `
<card-component>
Card body content
<footer>Card footer</footer>
</card-component>
`
})
class OtherComponent {}
In this example:
We don’t define header content, so we would use the default one defined in CardComponent.
We will use the body and footer defined here.
Take in consideration that default content in ng-content, even if written with conditional logic, is evaluated only once at component creation time. The conditional logic in fallback content behaves more like a static template than a dynamic one.
@Component({
selector: 'app-example',
standalone: true,
template: `
<ng-content>
@if (showDefault()) {
Default content
}
</ng-content>
`
})
export class ExampleComponent {
showDefault = input.required(true);
}
The showDefault
condition is checked only at creation, not dynamically.
Content Projection with ngTemplateOutlet
ngTemplateOutlet
is a structural directive in Angular that allows you to render a template dynamically in your component. It's part of Angular's template system and provides a way to reuse template fragments throughout your application.
The primary purpose of ngTemplateOutlet
is to insert a template fragment into a designated location in your component's template. This template can be defined elsewhere in your component or even in a completely separate component, making it a powerful tool for creating reusable UI elements.
@Component({
selector: 'app-dynamic-content',
standalone: true,
template: `
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-template #myTemplate>
<p>This content is rendered using ngTemplateOutlet</p>
</ng-template>
`
})
export class DynamicContentComponent {}
In this example:
We define a template using
<ng-template>
with a template reference variable#myTemplate
.We use
<ng-container>
with*ngTemplateOutlet
to render the template.The content inside
#myTemplate
will be rendered where thengTemplateOutlet
is placed.
Advanced Features
One of the powerful features of ngTemplateOutlet
is the ability to pass context to the template. This is done using ngTemplateOutletContext
. Here's an example:
@Component({
selector: 'app-advanced-example',
standalone: true,
template: `
<ng-container *ngTemplateOutlet="greetTemplate; context: { $implicit: 'World', position: 'Angular Developer' }"></ng-container>
<ng-template #greetTemplate let-name let-position="position">
<h2>Hello, {{ name }}!</h2>
<h3>{{ position }}</h3>
</ng-template>
`
})
export class AdvancedExampleComponent { }
In this example:
We pass a context object to the template using
ngTemplateOutletContext
.The
$implicit
property is a special property that can be accessed in the template without explicitly naming it.In the template, we use
let-name
to create a template input variable that receives the value from the context.And the next properties should be named (
let-position
).
Common mistakes to avoid
Not handling cases where content isn't projected
Misusing
@ContentChild
and@ContentChildren
Forgetting to use
ngAfterContentInit
for content-related logic
Content projection in Angular is a powerful feature that allows for the creation of flexible, reusable components. By mastering techniques like conditional projection, accessing projected content, and using ngTemplateOutlet
, you can build more adaptable and maintainable applications. Remember to consider performance implications and follow best practices to make the most of this feature in your Angular projects.
Thanks for reading so far 🙏
I’d like to have your feedback so please leave a comment, clap or follow. 👏
Spread the Angular love! 💜
If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥
Don't forget to follow me and stay updated: 📱
Thanks for being part of this Angular journey! 👋😁