Content Projection
In Angular ist die Wiederverwendbarkeit von Komponenten ein zentrales Konzept. Doch was passiert, wenn du dynamischen Inhalt in eine Komponente einfügen möchtest, ohne deren Struktur zu verändern? Genau hier kommt das mächtige Konzept der Content Projection ins Spiel – realisiert durch das <ng-content>
-Element.
Mit dieser Technik kannst du flexibel Inhalte aus dem Parent-Komponenten-Kontext in eine Child-Komponente einfügen. In diesem Artikel lernst du, wie ng-content
funktioniert, wann du es einsetzen solltest, und wie du damit auch komplexe Strukturen wie mehrere Projektionen oder selektive Platzhalter umsetzt.
Grundlegende Verwendung
Wenn man einen bestimmten Inhalt aus einem anderen Component in das aktuelle Component hineingeben möchte, kann man es sehr einfach mit ng-content
tun.
Ohne besondere Konfigurationen kann man dies mit Platzierung von ng-content
an der Stelle, an welcher der Inhalt projeziert werden soll, angeben.
import { Component } from '@angular/core';
@Component({
selector: 'custom-card',
imports: [],
template: `
<div class="custom-card">
<ng-content></ng-content>
</div>
`
})
export class CustomCard {}
Mit dieser Definition kann man nun dieses Component an einer anderen Stelle verwenden und Inhalte hineingeben, die zwischen <div class="custom-card">
und </div>
gerendert werden.
import { Component } from '@angular/core';
import { CustomCard } from './custom-card/custom-card.component.ts';
@Component({
selector: 'custom-card',
imports: [
CustomCard
],
template: `
<custom-card>
Dieser Inhalt wird in das Component hinein projeziert.
</custom-card>
`
})
export class CustomCard {}
Man kann selbstverständlich auch andere Elemente und weitere Components an dieser Stelle platzieren. Im Grunde dient ng-content
wie ein Platzhalter.
Im folgenden Beispiel sieht man, wie HTML-Tags und ein anderer Component in das aktuelle Component hineingegeben werden.
Zuerst definieren wir ein Placeholder-Component, das einfach nur einen statischen, formattierten Text ausgibt. Dadurch können wir sehen, dass gerenderte Inhalt aus diesem Component stamm.
import { Component } from '@angular/core';
@Component({
selector: 'app-placeholder',
imports: [],
template: `
<strong>
<i>Ich bin vom anderen Component.</i>
</strong>
`
})
export class PlaceholderComponent {}
Die Verwendung im Parent-Component oder einem anderen Component, in dem Content Projection zum Einsatz kommen soll, ist wie man es erwarten würde - einfach die Tags und Components verwenden, wie man es sonst tut.
import { Component } from '@angular/core';
import { CustomCard } from './custom-card/custom-card.component.ts';
import { PlaceholderComponent } from './placeholder/placeholder.component.ts';
@Component({
selector: 'app-placeholder',
imports: [
CustomCard,
PlaceholderComponent
],
template: `
<custom-card>
<h4>Eine H4-Überschrift</h4>
<app-placeholder></app-placeholder>
</custom-card>
`
})
export class CustomCard {}
Angular’s ng-content ist weder ein Component, noch ist es ein DOM-Element. Es ist ein Platzhalter, welcher Angular mitteilt, dass an dieser Stelle etwas ausgegeben (gerendert) werden soll. Alle ng-content Platzhalter werden während der Kompilierung ersetzt.
Wichtiger Hinweis
Man sollte ng-content
nicht mit Hilfe von Bedingungslogik und @if
, @for
oder @switch
einbinden. Angular wird immer Elemente für ng-content Platzhalter erstellen, auch wenn ng-content
versteckt ist.
Mehrere Slots
Manchmal möchte man nicht den gesamten Content an derselben Stelle platzieren, sondern z.B. einen Header-Teil, einen Body-Teil und einen Footer-Teil haben. Oder anders formuliert - man möchte Inhalte aufteilen.
An diesem Punkt kommen die sogenannten Slots zum Einsatz. Für die Auswahl des jeweiligen Ziel-Slots kommen CSS-Selektoren zum Einsatz.
Im folgenden Beispiel werden wir 2 Slots angeben und diese mit Selektoren ausstatten, sodass bei der Verwendung wir genau festlegen können, wohin welcher Inhalt geht.
Wir brauchen hauptsächlich in unserem Beispiel nur das Template.
<div class="custom-card-extended">
<div class="card-title">
<h3>
<ng-content
select=[card-title-slot]
></ng-content>
</h3>
</div>
<div class="card-title">
<ng-content
select=[card-body-slot]
></ng-content>
</div>
</div>
Hier wurden Selektoren als Element-Attribute verwendet. Die Verwendung dieses Components wird an einer anderen Stell wie folgt aussehen.
<app-card-extended>
<div card-title-slot>Hallo</div>
<div card-body-slot>
Hier kommt der Body der Card
</div>
</app-card-extended>
Standard Inhalte
Wenn man irgendwo ng-content
einsetzt und auch dann eine bestimmte Ausgabe bzw. Inhalt an dieser Stelle gerendert haben möchte, wenn keine Übereinstimmung mit den gewählten Selektoren gibt, kann man Standard-Inhalte platzieren. In anderen Worten: Wenn Angular nichts findet, was hier gerendert werden soll, verwendet es dann diese Standard-Inhalte.
<div class="custom-card">
<ng-content>
Hier steht ein Standard-Inhalt
</ng-content>
</div>
Die Verwendung kann dann auf zwei Wegen erfolgen. Man kann eigenen Inhalt übergeben oder, wenn man es nicht tut, es wird ein Standard-Inhalt ausgegeben, welchen man direkt im Component definiert hat.
<app-custom-card></app-custom-card>
<app-custom-card>
Dieser Inhalt wurde übergeben.
</app-custom-card>
Aliasing mit ngProjectAs
Manmal möchte man Tag- oder Attribut-Namen nicht fest vorgeben, sondern flexibel sein oder HTML-Standardelemente verwenden.
In diesem Fall hilft das sogenannte Aliasing.
<ng-content select="custom-title-slot"></ng-content>
Bei der Verwendung wird ngProjectAs
eingesetzt.
<app-custom-title>
<h3 ngProjectAs="custom-title-slot">
Custom title
</h3>
</app-custom-title>
Beispiel - Tabs
Im folgenden Beispiel wird ein Element Eigene Tabs zur Verdeutlichung von ng-content
gezeigt.
Für dieses Beispiel werden 2 Components gebraucht.
- Ein Container-Component. Dieses Component stellt den äußeren Container für alle einzelnen Tab-Elemente dar.
- Ein Element-Component. Dieses Component repräsentiert einen einzelnen Tab, welcher bestimmten (eigenen Inhalt) aufnehmen kann.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-tab',
imports: [],
templateUrl: './tab.component.html',
styleUrl: './tab.component.scss'
})
export class TabComponent {
title = input<string>('');
active = false;
}
Unser Tab-Component hat zwei Eigenschaften: title
und active
. Der Titel ist der Wert, welcher am Button zur Steuerung/Aktivierung des Tabs benötigt wird. Die Kennzeichnung als active
wird benötigt, um mitzuteilen, ob dieser Tab aktuell aktiv oder inaktiv ist.
Das Template ist relativ einfach aufgebaut. Der Inhalt wird nur dann ausgegeben, wenn dieser Tab tatsächlich aktiv ist.
@if (active) {
<div class="tab_pane">
<ng-content></ng-content>
</div>
}
Als nächsten benötigen wir ein Container, welcher die einzelnen Tabs in sich aufnehmen kann.
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TabComponent } from './tab/tab.component';
@Component({
selector: 'app-tabs',
imports: [
CommonModule
],
templateUrl: './tabs.component.html',
styleUrl: './tabs.component.scss'
})
export class TabsComponent implements AfterContentInit {
@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>
ngAfterContentInit(): void {
this.open(this.tabs.first);
}
/**
* Set tab active
* ---
* @param tab - Target tab
*/
open(tab: TabComponent): void {
this.tabs.forEach(t => t.active = false);
tab.active = true;
}
}
<div class="custom_tabs">
<div class="tab_triggers">
@for (tab of tabs; track tab) {
<button (click)="open(tab)" [ngClass]="{'active': tab.active}">
{{ tab.title() }}
</button>
}
</div>
<div class="tab_panes">
<ng-content></ng-content>
</div>
</div>
Die Verwendung ist dann relativ einfach. Wir staffeln einfach die einzelnen app-tab
Elemente in einem app-tabs
Container (Component) und geben die Inhalte an, die über ng-content
gerendert werden.
<app-tabs>
<app-tab title="First tab">
<p>Here is the content of first tab.</p>
</app-tab>
<app-tab title="Second tab">
<p>Here is the content of second tab.</p>
</app-tab>
<app-tab title="Third tab">
<p>Here is the content of third tab.</p>
</app-tab>
</app-tabs>