Reusable components often need the parent to decide part of the content. A card has a title, a body and a footer. A list has items that each parent wants to draw differently. Angular solves this with content projection and templates.
What you will learn
- Single and multi-slot projection with
ng-content - How
ng-templateandngTemplateOutletrender templates on demand - How a component receives a template from its parent with
contentChild
1. ng-content slots
// src/app/projection/panel.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-panel',
template: `
<section class="panel">
<header>
<h3>{{ heading() }}</h3>
<ng-content select="[panel-actions]" />
</header>
<div class="body"><ng-content /></div>
<footer><ng-content select="[panel-footer]" /></footer>
</section>
`,
styles: `
.panel { border: 1px solid #ddd; border-radius: .5rem; padding: 1rem; margin: 1rem 0; }
header { display: flex; justify-content: space-between; align-items: center; }
footer { color: #666; font-size: .9rem; }
`,
})
export class Panel {
readonly heading = input.required<string>();
}
The parent decides what goes in each slot:
<app-panel heading="Weekly report">
<button panel-actions type="button">Export</button>
<p>Sales grew by 12% this week.</p>
<small panel-footer>Updated today</small>
</app-panel>
Content without a select goes into the default <ng-content />. Elements with the attribute panel-actions go to that named slot.
2. ng-template and ngTemplateOutlet
An ng-template is a piece of template that is not rendered until you ask for it. ngTemplateOutlet renders it, and can pass data through a context.
// src/app/projection/list-with-template.ts
import { Component, contentChild, input, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-smart-list',
imports: [NgTemplateOutlet],
template: `
<ul>
@for (item of items(); track $index) {
<li>
@if (itemTemplate(); as tpl) {
<ng-container [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="{ $implicit: item, index: $index }" />
} @else {
{{ item }}
}
</li>
} @empty {
<li>Nothing here yet.</li>
}
</ul>
`,
})
export class SmartList<T = unknown> {
readonly items = input<T[]>([]);
// Finds <ng-template #itemTemplate> inside the component's content.
protected readonly itemTemplate = contentChild<TemplateRef<unknown>>('itemTemplate');
}
The parent supplies how each row looks:
// src/app/projection/projection-demo.ts
import { Component } from '@angular/core';
import { Panel } from './panel';
import { SmartList } from './list-with-template';
@Component({
selector: 'app-projection-demo',
imports: [Panel, SmartList],
template: `
<app-panel heading="Weekly report">
<button panel-actions type="button">Export</button>
<p>Sales grew by 12% this week.</p>
<small panel-footer>Updated today</small>
</app-panel>
<h3>Plain list</h3>
<app-smart-list [items]="fruits" />
<h3>Custom rows</h3>
<app-smart-list [items]="people">
<ng-template #itemTemplate let-person let-i="index">
<strong>{{ i + 1 }}. {{ person.name }}</strong> ({{ person.role }})
</ng-template>
</app-smart-list>
`,
})
export class ProjectionDemo {
protected readonly fruits = ['Apple', 'Banana', 'Cherry'];
protected readonly people = [
{ name: 'Asha', role: 'Developer' },
{ name: 'Ben', role: 'Designer' },
];
}
// src/app/app.ts
import { Component } from '@angular/core';
import { ProjectionDemo } from './projection/projection-demo';
@Component({
imports: [ProjectionDemo],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<h1>Projection</h1>
<app-projection-demo />
</main>
The key parts
ng-contentis a slot. Projected content is created by the parent and is not controlled by the component.ng-templateplusngTemplateOutletlets the component render the template once per item, with data passed in thecontext.let-personreads the$implicitvalue.let-i="index"reads the namedindexproperty.contentChildlooks inside the content projected by the parent. UseviewChildfor elements in the component's own template.- The component works with a default row, and the parent can override it. That is a flexible, still simple API.
Try it yourself
- Add a
headertemplate toSmartListin the same way asitemTemplate. - Add an empty-state template slot that the parent can customise.
- Build a
Tabscomponent that usesng-contentandcontentChildren.
Quick check
- What does the
selectattribute ofng-contentdo? - What is the difference between
contentChildandviewChild? - How do you pass data into a template rendered with
ngTemplateOutlet?
Next, create components at run time in dynamic components and host directives. See the content projection guide.