Eine Komponente ist dynamisch, wenn du zur Compile-Zeit nicht weißt, welche Komponente angezeigt werden muss — sie kommt aus einer Konfiguration, vom Server, aus einem CMS oder aus den Einstellungen des Benutzers. Angular gibt dir drei Ebenen von Werkzeugen; die Regel lautet, das einfachste zu wählen, das das Problem löst.
| Werkzeug | Wann | Inputs | Outputs | Lazy |
|---|---|---|---|---|
@if / @switch | kleine, bekannte Menge | ✓ normal | ✓ normal | mit @defer |
NgComponentOutlet | die Komponente ist ein Wert | ✓ inputs | ✕ | mit import() |
ViewContainerRef.createComponent | volle Kontrolle | ✓ inputBinding | ✓ outputBinding | ✓ |
createComponent() (Funktion) | außerhalb des Baums (Toast, Dialog) | ✓ | ✓ | ✓ |
Die Komplexitätsleiter
<!-- Ebene 1: Die Menge der Komponenten ist klein und bekannt → einfacher Control Flow -->
@switch (block().type) {
@case ('text') { <app-text-block [block]="block()" /> }
@case ('image') { <app-image-block [block]="block()" /> }
@case ('video') { <app-video-block [block]="block()" /> }
}
<!-- Ebene 2: Die Komponente ist ein WERT (kommt aus der Config, aus einer Map) → NgComponentOutlet -->
<ng-container *ngComponentOutlet="component(); inputs: { block: block() }" />
<!-- Ebene 3: Du brauchst Outputs, Direktiven, volle Kontrolle → ViewContainerRef.createComponent -->Ebene 2: NgComponentOutlet
import { NgComponentOutlet } from '@angular/common';
@Component({
imports: [NgComponentOutlet],
template: `
<ng-container *ngComponentOutlet="component(); inputs: inputs()" />
`,
})
export class BlockRenderer {
readonly block = input.required<Block>();
protected readonly component = computed(() => BLOCKS[this.block().type]); // Type<unknown>
protected readonly inputs = computed(() => ({ block: this.block() }));
}Ebene 3: ein Dashboard aus einer Konfiguration
Das komplette Muster: eine Registry (Typ → Lazy-import()), ein gemeinsamer Vertrag (das Widget-Interface) und ein Host, der die Komponente mit bindings erstellt (v20+). Jedes Widget ist ein eigener Chunk und wird erst geladen, wenn es in der Konfiguration auftaucht.
// Registry: Typ → Lazy Loading. Jedes Widget ist ein eigener Chunk.
export const WIDGET_REGISTRY: Record<WidgetType, () => Promise<Type<Widget>>> = {
stats: () => import('./stats-widget').then((m) => m.StatsWidget),
chart: () => import('./chart-widget').then((m) => m.ChartWidget),
quote: () => import('./quote-widget').then((m) => m.QuoteWidget),
todo: () => import('./todo-widget').then((m) => m.TodoWidget),
};
// Der gemeinsame Vertrag — jedes Widget hat title und remove
export interface Widget {
readonly title: InputSignal<string>;
readonly remove: OutputEmitterRef<void>;
}„Code wird zehnmal öfter gelesen als geschrieben.”
Aktuelle Konfiguration (die könnte vom Server kommen)
[
{
"id": 1,
"type": "stats",
"title": "dynamic-components:today"
},
{
"id": 2,
"type": "chart",
"title": "dynamic-components:traffic"
},
{
"id": 3,
"type": "quote",
"title": "dynamic-components:quote-of-the-day"
}
]bindings verknüpfen die Inputs mit Signals oder Funktionen — sie aktualisieren sich von selbst, ohne wiederholtes setInput() — und die Outputs mit Funktionen, ohne manuelles subscribe/unsubscribe. Du kannst an die erstellte Komponente auch Direktiven anhängen (directives: [...]). In v22 wurden ComponentFactoryResolver und ComponentFactory entfernt: Du übergibst die Klasse direkt.
// ✗ Vor v20: setInput + manuelles subscribe + ComponentFactoryResolver (in v22 entfernt!)
const factory = this.resolver.resolveComponentFactory(StatsWidget); // gibt es in v22 nicht mehr
const ref = this.vcr.createComponent(factory);
ref.setInput('title', this.config.title); // muss bei jeder Änderung erneut aufgerufen werden
const sub = ref.instance.remove.subscribe(() => this.removed.emit());
// + manuelles unsubscribe beim DestroyAußerhalb des Baums: createComponent()
Für Toasts, Dialoge oder Overlays muss die Komponente direkt an <body> angehängt werden, nicht dort, wo sie aufgerufen wird. Die Funktion createComponent + ApplicationRef.attachView erledigen das. (In echten Anwendungen nutzt du oft @angular/cdk/overlay oder den CDK-Dialog, die genau das für dich übernehmen.)
// Eine Komponente, die außerhalb des Baums erstellt wird (Dialog, Toast, globaler Tooltip)
@Service()
export class ToastService {
private readonly appRef = inject(ApplicationRef);
private readonly injector = inject(EnvironmentInjector);
show(message: string, kind: 'info' | 'ok' | 'bad' = 'info', ms = 3000) {
const ref = createComponent(Toast, {
environmentInjector: this.injector,
bindings: [
inputBinding('message', () => message),
inputBinding('kind', () => kind),
outputBinding('closed', () => close()),
],
});
const close = () => {
this.appRef.detachView(ref.hostView);
ref.destroy();
};
this.appRef.attachView(ref.hostView); // geht in die Change Detection ein
document.body.appendChild(ref.location.nativeElement);
setTimeout(close, ms);
}
}Wenn du eine Komponente nur später laden willst, ist @defer — Lazy Loading in Templates einfacher. Fehler in dynamisch erzeugten Komponenten fängst du mit onError: Fehler: @boundary & ErrorHandler.
Wann ja, wann nein
- Ein vom Benutzer konfigurierbares Layout (Dashboard, Widgets).
- Seiten, die aus einem CMS / Page Builder aufgebaut werden (ein Block „Text“, „Galerie“, „Formular“).
- Formulare, die aus einem JSON-Schema generiert werden.
- Plugins: Module, die hinzukommen, ohne die Host-Komponente zu ändern.
- Overlays: Toast, Dialog, Tooltip, Kontextmenü.
- Du hast 2–5 bekannte Varianten →
@switchist einfacher, lesbarer und wird zur Compile-Zeit geprüft. - Du willst nur Lazy Loading →
@deferoderloadComponentauf einer Route. - Du willst nur etwas ein- oder ausblenden →
@if.
Häufige Stolperfallen
- Du verlierst die Typprüfung: Der Name in
inputBinding('title', ...)ist ein String. Definiere einen Vertrag (ein Interface) und halte ihn nah an der Registry. - Memory Leaks bei
createComponentaußerhalb des Baums: Vergissref.destroy()unddetachViewnicht. - Neuerstellung bei jeder Änderung: Erstelle die Komponente nur neu, wenn sich der Typ ändert; der Rest läuft über reaktive Bindings.
- Der Injector: Eine erstellte Komponente erbt den Injector des
ViewContainerRef. Für einen anderen Kontext übergib eineninjectorin den Optionen.