Docs
/
Angular
Chapter 13
13 — Content Projection
What is Content Projection?
Content projection lets a parent pass HTML content into a child component's template — similar to React's children or Vue's slots.
Single-Slot Projection (<ng-content>)
// card.component.ts
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<ng-content /> <!-- Parent's content renders here -->
</div>
`,
})
export class CardComponent {}
<!-- Usage -->
<app-card>
<h2>Hello World</h2>
<p>This content is projected into the card.</p>
</app-card>
Multi-Slot Projection (select)
@Component({
selector: 'app-modal',
template: `
<div class="modal">
<header>
<ng-content select="[modal-header]" />
</header>
<div class="body">
<ng-content /> <!-- Default slot (no select) -->
</div>
<footer>
<ng-content select="[modal-footer]" />
</footer>
</div>
`,
})
export class ModalComponent {}
<app-modal>
<div modal-header>
<h2>Confirm Delete</h2>
</div>
<p>Are you sure you want to delete this item?</p>
<div modal-footer>
<button (click)="cancel()">Cancel</button>
<button (click)="confirm()">Delete</button>
</div>
</app-modal>
Select By
| Selector | Example |
|---|---|
| Element | select="h2" |
| Attribute | select="[modal-header]" |
| CSS class | select=".header" |
| Component | select="app-header" |
Conditional Content (@ContentChild)
@Component({
selector: 'app-card',
template: `
<div class="card">
@if (hasHeader) {
<header><ng-content select="[card-header]" /></header>
}
<div class="body">
<ng-content />
</div>
</div>
`,
})
export class CardComponent {
hasHeader = contentChild<ElementRef>('[card-header]');
// Or older: @ContentChild('cardHeader') header?: ElementRef;
}
<ng-template> — Deferred Template
<ng-template> defines a template that is not rendered by default — it must be explicitly instantiated.
<!-- This is NOT rendered automatically -->
<ng-template #greeting>
<h1>Hello, {{ user.name }}!</h1>
</ng-template>
<!-- Render it conditionally with ngTemplateOutlet -->
<div *ngTemplateOutlet="greeting"></div>
<ng-container> — Grouping Without Extra DOM
<ng-container> is a logical container that doesn't add any element to the DOM.
<!-- ❌ Adds an extra <div> to the DOM -->
<div *ngIf="user">
<h1>{{ user.name }}</h1>
</div>
<!-- ✅ No extra DOM element -->
<ng-container *ngIf="user">
<h1>{{ user.name }}</h1>
</ng-container>
<!-- With new control flow, ng-container is less needed -->
@if (user) {
<h1>{{ user.name }}</h1>
}
ngTemplateOutlet — Dynamic Templates
Render a template reference dynamically, optionally passing context data.
@Component({
template: `
<!-- Define templates -->
<ng-template #loading>
<app-spinner />
</ng-template>
<ng-template #error let-message="message">
<div class="error">{{ message }}</div>
</ng-template>
<ng-template #content let-data="data">
<h1>{{ data.title }}</h1>
<p>{{ data.body }}</p>
</ng-template>
<!-- Render dynamically based on state -->
<ng-container [ngTemplateOutlet]="currentTemplate"
[ngTemplateOutletContext]="currentContext">
</ng-container>
`,
})
export class DynamicComponent {
@ViewChild('loading') loadingTpl!: TemplateRef<any>;
@ViewChild('error') errorTpl!: TemplateRef<any>;
@ViewChild('content') contentTpl!: TemplateRef<any>;
currentTemplate!: TemplateRef<any>;
currentContext: any = {};
showLoading() {
this.currentTemplate = this.loadingTpl;
}
showError(message: string) {
this.currentTemplate = this.errorTpl;
this.currentContext = { message };
}
showContent(data: any) {
this.currentTemplate = this.contentTpl;
this.currentContext = { data };
}
}
Reusable List Component with Custom Template
@Component({
selector: 'app-data-list',
standalone: true,
imports: [NgTemplateOutlet],
template: `
@if (items.length === 0) {
<ng-content select="[empty]" />
} @else {
@for (item of items; track trackFn(item)) {
<ng-container
[ngTemplateOutlet]="itemTemplate"
[ngTemplateOutletContext]="{ $implicit: item, index: $index }"
/>
}
}
`,
})
export class DataListComponent<T> {
@Input() items: T[] = [];
@Input() trackFn: (item: T) => any = (item: any) => item;
@ContentChild('itemTemplate') itemTemplate!: TemplateRef<any>;
}
<!-- Usage — parent defines how each item looks -->
<app-data-list [items]="users" [trackFn]="trackById">
<ng-template #itemTemplate let-user let-i="index">
<div class="user-row">
{{ i + 1 }}. {{ user.name }} — {{ user.email }}
</div>
</ng-template>
<p empty>No users found.</p>
</app-data-list>
@ContentChild / @ContentChildren
Access projected content from the child component.
@Component({
selector: 'app-tabs',
template: `
<div class="tab-headers">
@for (tab of tabs(); track tab.label) {
<button [class.active]="tab === activeTab()" (click)="selectTab(tab)">
{{ tab.label }}
</button>
}
</div>
<div class="tab-body">
<ng-content />
</div>
`,
})
export class TabsComponent {
tabs = contentChildren(TabComponent);
activeTab = signal<TabComponent | null>(null);
ngAfterContentInit() {
if (this.tabs().length > 0) {
this.selectTab(this.tabs()[0]);
}
}
selectTab(tab: TabComponent) {
this.tabs().forEach(t => t.active.set(false));
tab.active.set(true);
this.activeTab.set(tab);
}
}
Key Takeaways
<ng-content>is Angular's content projection (slot) mechanism- Use
selectattribute for multi-slot projection (header, body, footer) <ng-template>defines a lazy template — not rendered until explicitly used<ng-container>groups elements without adding DOM nodesngTemplateOutletrenders templates dynamically with context — great for reusable componentslet-variableinng-templatereceives context;$implicitis the default variable- Use
@ContentChild/contentChild()to access projected content programmatically