Angular Content Projection
Introduction
Content projection is a powerful pattern in Angular that allows you to insert (or "project") content from a parent component into the child component's template. This technique enhances component reusability by creating more flexible and adaptable components.
Think of content projection like creating a "slot" in your component where other components can insert their own content. This is similar to how HTML elements can contain other elements. Angular's content projection system formalizes this pattern and provides several advanced features beyond basic containment.
Basic Content Projection
How It Works
The most basic form of content projection uses the <ng-content>
element, which serves as a placeholder where the content will be projected.
Simple Example
Let's create a reusable card component that can display different content using projection:
Step 1: Create a Card Component
// card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<h2>{{ title }}</h2>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>
`,
styles: [`
.card {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #f5f5f5;
padding: 10px;
border-bottom: 1px solid #ccc;
}
.card-body {
padding: 15px;
}
`]
})
export class CardComponent {
title = 'Card Title';
}
Step 2: Use the Card Component with Content Projection
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-card>
<p>This content will be projected into the card body.</p>
<button>Click me!</button>
</app-card>
<app-card>
<img src="assets/image.jpg" alt="Sample image" />
<p>A different card with an image and text.</p>
</app-card>
`
})
export class AppComponent {}
Result
The rendered output would be two cards, each containing different content:
- First card: A paragraph and a button
- Second card: An image and a paragraph
This demonstrates how the same component can be reused with different content.
Multi-slot Content Projection
Sometimes you want to project content into multiple specific places in your component. Angular supports this with named slots using the select
attribute on <ng-content>
elements.
Example with Named Slots
Let's improve our card component to support a header and footer:
// enhanced-card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-enhanced-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
`,
styles: [`
.card {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 20px;
}
.card-header {
background-color: #f5f5f5;
padding: 10px;
border-bottom: 1px solid #ccc;
}
.card-body {
padding: 15px;
}
.card-footer {
background-color: #f5f5f5;
padding: 10px;
border-top: 1px solid #ccc;
}
`]
})
export class EnhancedCardComponent {}
Now we can use this component with named projections:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<app-enhanced-card>
<div card-header>
<h2>User Profile</h2>
</div>
<div class="profile-content">
<img src="assets/profile.jpg" alt="User Profile" />
<p>Name: Jane Doe</p>
<p>Email: [email protected]</p>
</div>
<div card-footer>
<button>Edit Profile</button>
<button>Change Password</button>
</div>
</app-enhanced-card>
`
})
export class AppComponent {}
In this example:
- Content with the
card-header
attribute goes into the header slot - Content with the
card-footer
attribute goes into the footer slot - All other content goes into the default slot (the card body)
Selection Using Different Criteria
The select
attribute on <ng-content>
can match elements using:
-
Element selectors:
html<ng-content select="header"></ng-content>
-
CSS class selectors:
html<ng-content select=".header-content"></ng-content>
-
Attribute selectors (as seen in our previous example):
html<ng-content select="[card-header]"></ng-content>
-
Combining selectors:
html<ng-content select="section.important"></ng-content>
Conditional Content Projection
Sometimes you might want to check if content was projected. You can use ngProjectAs
with <ng-container>
to wrap projected content and check its presence:
// conditional-card.component.ts
import { Component, ContentChild } from '@angular/core';
@Component({
selector: 'app-conditional-card',
template: `
<div class="card">
<div class="card-header" *ngIf="hasHeaderContent">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>
`
})
export class ConditionalCardComponent {
@ContentChild('cardHeader') headerContent;
get hasHeaderContent(): boolean {
return !!this.headerContent;
}
}
Real-world Example: Reusable Modal Component
Let's create a practical example of a reusable modal component using content projection:
// modal.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-modal',
template: `
<div class="modal-backdrop" [class.visible]="isOpen" (click)="closeModal()">
<div class="modal-container" (click)="$event.stopPropagation()">
<div class="modal-header">
<ng-content select="[modal-title]"></ng-content>
<button class="close-btn" (click)="closeModal()">×</button>
</div>
<div class="modal-body">
<ng-content select="[modal-body]"></ng-content>
</div>
<div class="modal-footer">
<ng-content select="[modal-footer]"></ng-content>
</div>
</div>
</div>
`,
styles: [`
.modal-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-backdrop.visible {
display: flex;
justify-content: center;
align-items: center;
}
.modal-container {
background: white;
border-radius: 5px;
width: 500px;
max-width: 90%;
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 15px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
}
.close-btn {
border: none;
background: none;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
}
`]
})
export class ModalComponent {
@Input() isOpen = false;
@Output() closed = new EventEmitter<void>();
closeModal(): void {
this.isOpen = false;
this.closed.emit();
}
}
Using this modal component in a parent component:
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="openModal()">Open Confirmation Dialog</button>
<app-modal [isOpen]="showModal" (closed)="onModalClosed()">
<h2 modal-title>Confirm Action</h2>
<div modal-body>
<p>Are you sure you want to perform this action? This cannot be undone.</p>
</div>
<div modal-footer>
<button class="btn btn-secondary" (click)="onModalClosed()">Cancel</button>
<button class="btn btn-primary" (click)="confirmAction()">Confirm</button>
</div>
</app-modal>
`
})
export class AppComponent {
showModal = false;
openModal(): void {
this.showModal = true;
}
onModalClosed(): void {
this.showModal = false;
}
confirmAction(): void {
// Handle the confirmation action
console.log('Action confirmed!');
this.showModal = false;
}
}
This modal component is highly reusable—you can project different content into the title, body, and footer sections to create various types of modals (confirmation dialogs, information dialogs, forms, etc.).
Advanced Content Projection with ngTemplateOutlet
For even more control over projected content, you can combine content projection with ngTemplateOutlet
. This technique allows you to:
- Project templates rather than simple content
- Pass context data into the templates
Here's an example creating a data list component:
// data-list.component.ts
import { Component, Input, ContentChild, TemplateRef } from '@angular/core';
@Component({
selector: 'app-data-list',
template: `
<div class="list-container">
<div *ngFor="let item of items" class="list-item">
<ng-container *ngTemplateOutlet="itemTemplate || defaultTemplate; context: {$implicit: item}"></ng-container>
</div>
</div>
<ng-template #defaultTemplate let-item>
{{ item | json }}
</ng-template>
`
})
export class DataListComponent {
@Input() items: any[] = [];
@ContentChild('itemTemplate') itemTemplate: TemplateRef<any>;
}
Using the data list with custom templates:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<app-data-list [items]="users">
<ng-template #itemTemplate let-user>
<div class="user-card">
<img [src]="user.avatar" alt="{{ user.name }}" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</ng-template>
</app-data-list>
`
})
export class AppComponent {
users = [
{ id: 1, name: 'John Doe', email: '[email protected]', avatar: 'assets/john.png' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', avatar: 'assets/jane.png' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', avatar: 'assets/bob.png' }
];
}
Best Practices for Content Projection
- Use named slots when your component needs to project content into multiple locations
- Provide sensible defaults for optional projections
- Avoid deep nesting of projected content to maintain readability
- Document the available projection slots so other developers understand how to use your component
- Use
ng-container
for grouping elements without introducing extra DOM nodes - Consider component styles and how they might affect projected content
Common Pitfalls
- CSS styling conflicts: Be aware that the parent component's styles may affect projected content
- Using structural directives directly on
ng-content
: This won't work; use a wrapper element instead - Overusing content projection: For simple data scenarios, inputs might be clearer
- Not checking for projected content: Use
ContentChild
orContentChildren
to verify if content was provided
Summary
Content projection is a powerful feature in Angular that enables you to create more flexible and reusable components. By using the <ng-content>
element, you can project content from parent components into child components, either using a single slot or multiple named slots.
We've explored:
- Basic content projection using
<ng-content>
- Multi-slot content projection with selectors
- Conditional content projection
- Advanced techniques using
ngTemplateOutlet
- Real-world examples with cards and modals
Content projection is a core building block for creating component libraries and complex UI systems in Angular applications.
Additional Resources
- Angular Official Documentation on Content Projection
- Component Interaction Best Practices
- Advanced Components - Angular University
Exercises
- Create a tabbed container component using content projection that allows users to project different tab content.
- Extend the modal component to support different sizes (small, medium, large) and animations.
- Build a data table component that uses content projection for custom column templates.
- Create a card component that conditionally displays a header only if content is projected into the header slot.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)