Content Projection bedeutet, dass eine Komponente Inhalt anzeigt, den sie von ihrem Parent erhält und der zwischen ihren Tags steht. Das ist der Mechanismus hinter jeder „Container“-Komponente: Card, Dialog, Layout, Panel, Tab. Die Komponente legt mit <ng-content> fest, wo der Inhalt erscheint; der Parent entscheidet, was erscheint.
Mehrere Slots mit select
@Component({
selector: 'app-card',
template: `
<header>
<ng-content select="[card-title]">Kein Titel</ng-content> <!-- Fallback (v18+) -->
<ng-content select="[card-actions]" />
</header>
<div class="body">
<ng-content /> <!-- der Standard-Slot: der Rest -->
</div>
<footer>
<ng-content select="app-card-footer" /> <!-- nach dem Tag -->
</footer>
`,
})
export class Card {}3 Produkte · 1.249 RON · Lieferung morgen.
Card ohne Titel und ohne Footer: Leere Slots nutzen den Fallback oder verschwinden.
ngProjectAsNützlich, wenn der Wrapper das Auswahl-Attribut nicht tragen kann.
Was in select stehen kann
<ng-content select="app-card-footer" /> <!-- tag -->
<ng-content select="[card-title]" /> <!-- Attribut -->
<ng-content select=".highlight" /> <!-- Klasse -->
<ng-content select="h2, h3" /> <!-- Liste von Selektoren -->
<ng-content select="button:not(.secondary)" /> <!-- :not() -->
<ng-content /> <!-- alles, was die anderen nicht erfasst haben -->ngProjectAs
<!-- Du willst mehrere Elemente in einen Slot projizieren, ohne Wrapper im DOM -->
<app-card>
<ng-container ngProjectAs="[card-title]">
<app-icon name="box" /> Bestellung #1042
</ng-container>
</app-card>Die wichtige Falle: Projizierter Inhalt wird trotzdem erzeugt
Projizierte Komponenten werden vom Parent instanziiert, als Teil des Templates des Parents. Setzt das Kind <ng-content> in ein @if, wird nur das DOM ausgeblendet — die Komponenten existieren bereits, ihre Konstruktoren sind gelaufen, Requests wurden gestartet. Außerdem lässt sich <ng-content> nicht zweimal „projizieren“. Die Lösung: ein <ng-template> entgegennehmen und es erst instanziieren, wenn du es wirklich brauchst.
- ExpensiveWidget #2 ERZEUGT
// ✓ Nimmt ein Template entgegen und instanziiert es nur bei Bedarf
@Component({
selector: 'app-collapsible',
imports: [NgTemplateOutlet],
template: `
<button (click)="open.set(!open())">Toggle</button>
@if (open()) { <ng-container [ngTemplateOutlet]="content() ?? null" /> }
`,
})
export class Collapsible {
readonly content = contentChild(TemplateRef);
readonly open = signal(false);
}
// Verwendung:
// <app-collapsible>
// <ng-template><app-heavy-report /></ng-template>
// </app-collapsible>// ✗ Der projizierte Inhalt wird vom PARENT erzeugt, egal ob das Kind ihn anzeigt oder nicht.
@Component({
selector: 'app-collapsible',
template: `
<button (click)="open.set(!open())">Toggle</button>
@if (open()) { <ng-content /> } <!-- blendet das DOM aus, aber die Komponenten existieren bereits -->
`,
})
export class Collapsible {
readonly open = signal(false);
}Projection oder Input?
// Projection vs. Input — wie entscheidest du?
// Input: Die Komponente steuert, WIE die Daten aussehen (festes, einheitliches Format)
<app-user-chip [user]="user()" />
// Projection: Der Parent steuert, WAS erscheint (freies Markup, flexibel)
<app-dialog>
<h2 dialog-title>Projekt löschen?</h2>
<p>Diese Aktion kann nicht rückgängig gemacht werden.</p>
<button dialog-actions (click)="confirm()">Löschen</button>
</app-dialog>- Container-Komponenten: Card, Dialog, Drawer, Layout, Panel.
- Der Parent braucht freies Markup (Links, Icons, eigene Buttons).
- Du willst Dutzende Inputs wie
titleIcon,titleColor,showTitleBadge... vermeiden.
- Die Daten müssen immer gleich aussehen (visuelle Konsistenz) → Inputs.
- Die Komponente muss die Daten verarbeiten (sortieren, formatieren) → Inputs.
- Der Inhalt muss wiederholt oder bedingt erzeugt werden → ng-template.
Wie du erkennst, ob ein Slot leer ist
Eine Karte mit Fußzeile muss manchmal auch den Rahmen der Fußzeile ausblenden, wenn nichts übergeben wird. Du hast zwei Möglichkeiten. Bekommt der Slot eine Komponente oder Direktive, sagt dir contentChild(), ob sie existiert. Ist es nur Markup, reicht CSS :empty auf dem Element um den Slot.
@Component({
selector: 'app-card',
template: `
<div class="body"><ng-content /></div>
@if (footer()) {
<!-- die Bedingung liegt am Rahmen; der Inhalt der Fußzeile wird trotzdem erzeugt -->
<footer><ng-content select="app-card-footer" /></footer>
}
`,
})
export class Card {
protected readonly footer = contentChild(CardFooter); // gibt es ein <app-card-footer> im Inhalt?
}
/* oder, für einfaches Markup: */
.actions:empty {
display: none;
}Wer den projizierten Inhalt stylt
Projizierter Inhalt gehört dem Parent: Er entsteht in dessen Template und bekommt daher die Styles des Parents, nicht die der Komponente, die ihn anzeigt. Die Styles der Komponente gelten nur für ihre eigenen Elemente. Die Komponente stylt also den „Rahmen“ (Abstände, Rand, Position), der Parent das, was er hineinlegt. Musst du den projizierten Inhalt trotzdem erreichen, nimm CSS-Variablen auf :host (siehe Komponenten-Styles).
/* card.css — die Komponente stylt den Rahmen */
:host {
display: block;
padding: var(--card-padding, 1rem);
border: 1px solid var(--border);
}
p {
color: red; /* ✗ erreicht das per Projektion erhaltene <p> nicht */
}
/* page.css — der Parent stylt, was er in die Karte legt */
app-card p {
color: var(--text-2);
}Häufige Stolperfallen
- select auf Elementen innerhalb von @if/@for des Parents: Das funktioniert nur, wenn das Element mit dem Attribut die „Wurzel“ des Blocks ist; sonst landet es im Standard-Slot. Nutze
ngProjectAsauf einemng-container. - Dasselbe
<ng-content>zweimal: Der Inhalt erscheint nur im letzten. Für Wiederholung nimm Templates. - Projizierten Inhalt stylen: Das CSS der Komponente erreicht die Elemente des Parents nicht (Encapsulation). Style den Wrapper oder nutze CSS-Variablen.
Spickzettel
<ng-content /> <!-- der Standard-Slot: alles, was kein anderes select fängt -->
<ng-content select="[card-title]" /> <!-- nach Attribut -->
<ng-content select="app-card-footer" /> <!-- nach Tag -->
<ng-content select="h2, h3" /> <!-- eine Liste von Selektoren -->
<ng-content select="[card-title]">Kein Titel</ng-content> <!-- Standardinhalt (v18+) -->
<h2 ngProjectAs="[card-title]">...</h2> <!-- sieht für select wie ein anderes Element aus -->
<!-- projizierter Inhalt wird IMMER erzeugt, auch wenn der Slot versteckt ist → Erzeugung bei Bedarf: ng-template -->
<!-- die Styles des projizierten Inhalts kommen vom Parent -->