ViewContainerRef und TemplateRef sind die zwei Hälften desselben Mechanismus: Ein <ng-template> deklariert einen Template-Block als nicht gerenderte Variable, ein ViewContainerRef ist der Anker im Component-Tree, an dem dieser Block (oder eine vollständige Component) zur Laufzeit eingehängt wird. Dazwischen sitzt EmbeddedViewRef — das Handle für eine konkret eingesetzte Instanz, mit dem du Context-Daten aktualisierst, die DOM-Knoten greifst oder die View wieder zerstörst.
Dieser Artikel zeigt mit Angular 21 (aktueller Stand), wie das mentale Modell funktioniert, wie du viewChild() mit TemplateRef typsicher kombinierst, wann *ngTemplateOutlet der bessere Weg ist als vcr.createEmbeddedView(), wie eine eigene Strukturdirektive *appWhen aufgebaut wird und wo Cleanup-Pflichten lauern, die ohne expliziten destroy()-Aufruf zu Memory-Leaks führen.
Mental Model: Schablone und Anker
Ein Angular-Template, das du zwischen <ng-template>...</ng-template> schreibst, wird nicht automatisch gerendert. Der Compiler übersetzt es in eine Factory-Funktion und macht sie als TemplateRef<C> zugänglich — eine Schablone, die du später n mal instanziieren kannst. Bis dahin existiert kein DOM, keine Bindings laufen, keine Effects feuern.
Damit aus der Schablone tatsächlich DOM wird, brauchst du einen Insertion-Point: einen ViewContainerRef. Das ist ein logischer Anker im Component-Tree, an dem Angular eine oder mehrere Views (Embedded-Views aus Templates oder Host-Views von Components) einhängen darf. Jeder ViewContainer hat einen Index, ein Anchor-Element, einen eigenen Injector — und eine API zum Erzeugen, Verschieben und Zerstören seiner Kinder.
Die Beziehung lässt sich in einem Satz zusammenfassen: TemplateRef ist die Definition, ViewContainerRef der Ort, EmbeddedViewRef die laufende Instanz dazwischen. Genau diese Dreiteilung sieht man in jedem *ngIf, *ngFor oder @if — der Unterschied ist nur, dass Angular dort die mechanische Arbeit für dich erledigt.
Deklaration ohne Render
<ng-template> ist im Markup sichtbar, im DOM aber nicht. Der Browser bekommt an dieser Stelle einen Kommentar-Knoten als Anker — der eigentliche Inhalt schlummert als kompilierte Schablone. Erst wenn jemand das Template instanziiert (eine Strukturdirektive, ein *ngTemplateOutlet, ein expliziter createEmbeddedView()-Call), entsteht DOM.
<!-- Wird NICHT gerendert: kein Insertion-Point -->
<ng-template>
<p>Diesen Text siehst du nie.</p>
</ng-template>
<!-- Mit Reference-Variable als Variable haltbar -->
<ng-template #greeting let-name>
<p>Hallo, {{ name }}!</p>
</ng-template>
<!-- Erst dieser Outlet macht das Template sichtbar -->
<ng-container
*ngTemplateOutlet="greeting; context: { $implicit: 'Anna' }"
/>Die Reference-Variable #greeting macht aus dem Template eine ansprechbare Größe. Du kannst sie an *ngTemplateOutlet übergeben, an einen Input einer Child-Component reichen oder per viewChild() programmatisch holen.
Drei Eigenheiten von <ng-template> sind wichtig:
- Keine eigenen DOM-Knoten. Das Element selbst rendert nie — auch nicht als leerer Wrapper. Stilregeln, die du auf
ng-templatesetzt, greifen nirgends. - Eigener Binding-Scope. Variablen aus
let-fooleben nur innerhalb des Templates;@Input-Werte aus der Aufruf-Component sind weiterhin sichtbar. - Faules Verhalten. Bindings im Template laufen nur, wenn eine konkrete Instanz existiert. Ein nicht eingehängtes Template verbraucht keine CD-Zeit.
Klassisch und modern, mit Timing-Unterschied
Um aus dem TypeScript-Code an ein Template zu kommen, brauchst du eine Query auf seine Reference-Variable. Es gibt zwei Stile: das klassische @ViewChild und die moderne Signal-basierte viewChild()-Funktion.
import {
Component,
TemplateRef,
ViewChild,
viewChild,
AfterViewInit,
effect,
} from '@angular/core';
@Component({
selector: 'app-template-query',
standalone: true,
template: `
<ng-template #greeting let-name>
<p>Hallo, {{ name }}!</p>
</ng-template>
`,
})
export class TemplateQueryComponent implements AfterViewInit {
// Klassisch: undefined bis ngAfterViewInit
@ViewChild('greeting', { static: false })
classic!: TemplateRef<{ $implicit: string }>;
// Modern: Signal, automatisch reaktiv
modern = viewChild<TemplateRef<{ $implicit: string }>>('greeting');
constructor() {
// Effect feuert, sobald der Signal-Query auflöst
effect(() => {
const tpl = this.modern();
if (tpl) {
console.log('TemplateRef bereit:', tpl);
}
});
}
ngAfterViewInit() {
console.log('Klassisch:', this.classic);
}
}Die Signal-Variante hat zwei Vorteile, die in der Praxis Boilerplate sparen: Sie ist von Anfang an als Wert ansprechbar (initial undefined, danach ein gefülltes TemplateRef), und sie integriert sich nahtlos mit effect() und computed() — du hängst Reaktionslogik direkt ans Template, ohne Lifecycle-Hook und ohne BehaviorSubject-Brücke.
Anker programmatisch nutzen
ViewContainerRef bekommst du auf zwei Arten: Per inject(ViewContainerRef) in einer Component oder Directive (dann zeigt er auf die Position des eigenen Hosts) oder per viewChild('anchor', { read: ViewContainerRef }) an einem expliziten Anker-Element im Template.
import {
Component,
TemplateRef,
ViewContainerRef,
viewChild,
inject,
EmbeddedViewRef,
} from '@angular/core';
@Component({
selector: 'app-anchor-demo',
standalone: true,
template: `
<button (click)="show()">Einhängen</button>
<button (click)="clear()">Leeren</button>
<ng-template #tpl let-name>
<p>Hallo, {{ name }}!</p>
</ng-template>
<!-- Insertion-Point: hier wird die Embedded-View eingefügt -->
<ng-container #host />
`,
})
export class AnchorDemoComponent {
tpl = viewChild.required<TemplateRef<{ $implicit: string }>>('tpl');
host = viewChild.required('host', { read: ViewContainerRef });
private current: EmbeddedViewRef<{ $implicit: string }> | null = null;
show() {
this.current?.destroy();
this.current = this.host().createEmbeddedView(this.tpl(), {
$implicit: 'Anna',
});
}
clear() {
this.host().clear(); // zerstört alle Views in diesem Container
this.current = null;
}
}createEmbeddedView() gibt dir ein EmbeddedViewRef zurück — der Rückkanal für Updates und Cleanup. Wichtig: Hätten wir hier inject(ViewContainerRef) im Konstruktor verwendet, wäre die View neben dem Component-Tag eingefügt worden, nicht innerhalb. Der <ng-container #host /> plus read: ViewContainerRef löst genau dieses Positionierungsproblem.
$implicit und benannte let-Variablen
Templates dürfen Daten von außen entgegennehmen. Diese Daten reichst du beim Instanziieren als Context-Object mit. Im Template selbst greifst du sie über let-...-Deklarationen ab. Eine Sonderrolle hat $implicit — der Default-Slot, den eine alleinstehende let-name-Deklaration ohne = greift.
import {
Component,
TemplateRef,
ViewContainerRef,
viewChild,
} from '@angular/core';
interface UserCtx {
$implicit: string; // Name
age: number;
role: 'admin' | 'user';
}
@Component({
selector: 'app-ctx-demo',
standalone: true,
template: `
<button (click)="render()">Rendern</button>
<ng-template #card let-name let-age="age" let-role="role">
<article>
<h3>{{ name }} ({{ age }})</h3>
<span>Rolle: {{ role }}</span>
</article>
</ng-template>
<ng-container #host />
`,
})
export class CtxDemoComponent {
tpl = viewChild.required<TemplateRef<UserCtx>>('card');
host = viewChild.required('host', { read: ViewContainerRef });
render() {
this.host().clear();
this.host().createEmbeddedView(this.tpl(), {
$implicit: 'Anna',
age: 31,
role: 'admin',
});
}
}let-name ohne = mappt auf $implicit, let-age="age" greift auf die gleichnamige Property im Context. Der Generic-Parameter TemplateRef<UserCtx> macht den Context typsicher — TypeScript meckert, sobald du age vergisst oder eine falsche Property tippst.
Imperatives sparen, Position fixieren
In den meisten Fällen brauchst du gar keinen ViewContainerRef und kein createEmbeddedView(). Wenn die Position im Template fest ist und nur der Inhalt variiert, ist *ngTemplateOutlet aus @angular/common der direkte Weg.
import { Component, signal, TemplateRef, viewChild } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
type Status = 'loading' | 'error' | 'ok';
@Component({
selector: 'app-status-card',
standalone: true,
imports: [NgTemplateOutlet],
template: `
<ng-template #loading><p>Lädt …</p></ng-template>
<ng-template #error let-msg><p class="err">{{ msg }}</p></ng-template>
<ng-template #ok let-data>
<p>Ergebnis: {{ data }}</p>
</ng-template>
<section>
<ng-container
*ngTemplateOutlet="
templateFor(status());
context: contextFor(status())
"
/>
</section>
<button (click)="next()">Status wechseln</button>
`,
})
export class StatusCardComponent {
status = signal<Status>('loading');
loading = viewChild.required<TemplateRef<unknown>>('loading');
error = viewChild.required<TemplateRef<{ $implicit: string }>>('error');
ok = viewChild.required<TemplateRef<{ $implicit: string }>>('ok');
templateFor(s: Status): TemplateRef<unknown> {
return s === 'loading' ? this.loading()
: s === 'error' ? this.error()
: this.ok();
}
contextFor(s: Status): Record<string, unknown> {
return s === 'error' ? { $implicit: 'Server-Fehler 500' }
: s === 'ok' ? { $implicit: '42 Treffer' }
: {};
}
next() {
const order: Status[] = ['loading', 'ok', 'error'];
this.status.set(order[(order.indexOf(this.status()) + 1) % 3]);
}
}*ngTemplateOutlet kümmert sich automatisch um Cleanup: Wechselt der Wert, zerstört die Direktive die alte Embedded-View und erzeugt eine neue. Du sparst dir den destroy()-Aufruf, das Tracking des aktuellen EmbeddedViewRef und die vcr.clear()-Logik.
Zwei Insertion-Modi am selben Anker
Ein ViewContainerRef kann beides: Embedded-Views aus Templates und Host-Views aus Components. Das ist kein Detail — es ist der ganze Charme der API. Welcher Weg passt, hängt davon ab, was du eigentlich einhängst.
| Aspekt | createEmbeddedView(tpl) | createComponent(Cmp) |
|---|---|---|
| Quelle | TemplateRef<C> | Type<T> (Component-Klasse) |
| Rückgabe | EmbeddedViewRef<C> | ComponentRef<T> |
| Lifecycle-Hooks | nur Template-Bindings | volle Hooks: ngOnInit, ngOnDestroy, … |
| Inputs/Outputs | Context-Object (let-) | setInput(), instance.output.subscribe() |
| Eigenes Component-DI | nein, übernimmt vom Anker | ja, eigene Injection-Hierarchie |
| Wiederverwendbarkeit | leicht (gleicher Tpl überall) | volle Komponenten-Logik |
| Typischer Use-Case | Slots, Renderer, Conditional Wrapper | Modals, Tabs, Plugin-Slots |
import {
Component,
ViewContainerRef,
TemplateRef,
viewChild,
inject,
} from '@angular/core';
@Component({
selector: 'app-greeting-card',
standalone: true,
template: `<p>Greeting aus echter Component</p>`,
})
export class GreetingCardComponent {}
@Component({
selector: 'app-side-by-side',
standalone: true,
template: `
<ng-template #tpl>
<p>Greeting aus Template</p>
</ng-template>
<ng-container #anchor />
<button (click)="useTemplate()">Template einhängen</button>
<button (click)="useComponent()">Component einhängen</button>
`,
})
export class SideBySideComponent {
tpl = viewChild.required<TemplateRef<unknown>>('tpl');
anchor = viewChild.required('anchor', { read: ViewContainerRef });
useTemplate() {
this.anchor().clear();
this.anchor().createEmbeddedView(this.tpl());
}
useComponent() {
this.anchor().clear();
const ref = this.anchor().createComponent(GreetingCardComponent);
// ref.instance, ref.setInput(), ref.destroy() stehen jetzt zur Verfügung
}
}Faustregel: Wenn das, was du einsetzt, eigenen State, Lifecycle-Hooks oder DI-Provider braucht, ist es eine Component. Wenn es nur ein Stück Markup mit ein paar Bindings ist, reicht ein Template.
Context, rootNodes, destroy
EmbeddedViewRef<C> ist das Handle, das createEmbeddedView() zurückgibt. Es bündelt drei Dinge, die du nach dem Einhängen brauchst: den aktuellen Context, die DOM-Knoten und die Lifecycle-Steuerung.
import {
Component,
TemplateRef,
ViewContainerRef,
viewChild,
EmbeddedViewRef,
DestroyRef,
inject,
} from '@angular/core';
interface Ctx {
$implicit: string;
count: number;
}
@Component({
selector: 'app-evr-demo',
standalone: true,
template: `
<ng-template #tpl let-name let-count="count">
<p>{{ name }} hat {{ count }} Klicks.</p>
</ng-template>
<ng-container #host />
<button (click)="mount()">Mount</button>
<button (click)="bump()">+1</button>
<button (click)="unmount()">Destroy</button>
`,
})
export class EvrDemoComponent {
tpl = viewChild.required<TemplateRef<Ctx>>('tpl');
host = viewChild.required('host', { read: ViewContainerRef });
private ref: EmbeddedViewRef<Ctx> | null = null;
constructor() {
// Auto-Cleanup beim Component-Destroy
inject(DestroyRef).onDestroy(() => this.ref?.destroy());
}
mount() {
this.ref?.destroy();
this.ref = this.host().createEmbeddedView(this.tpl(), {
$implicit: 'Anna',
count: 0,
});
// DOM-Zugriff via rootNodes
const first = this.ref.rootNodes[0] as HTMLElement;
first?.classList?.add('is-mounted');
}
bump() {
if (!this.ref) return;
// Context direkt mutieren …
this.ref.context.count += 1;
// … und CD anstoßen
this.ref.markForCheck();
}
unmount() {
this.ref?.destroy();
this.ref = null;
}
}Drei Punkte sind hier essenziell:
contextist nicht reaktiv. Wenn du Felder direkt mutierst, mussmarkForCheck()(OnPush) oderdetectChanges()(detached) folgen, sonst sieht das Template die Änderung nie.rootNodesliefert die DOM-Knoten der View — alle Top-Level-Elemente, denn ein Template kann mehrere Geschwister-Knoten haben.destroy()ist deine Verantwortung. Ohne expliziten Aufruf bleibt die View im Memory hängen, solange der ViewContainer lebt — also potenziell die ganze Component-Lebensdauer.
insert, move, indexOf
Ein ViewContainer ist eine geordnete Liste. Jede eingesetzte View bekommt einen Index, und du kannst sie verschieben, einsortieren oder gezielt entfernen — ohne sie neu zu erzeugen.
import {
Component,
TemplateRef,
ViewContainerRef,
viewChild,
EmbeddedViewRef,
} from '@angular/core';
@Component({
selector: 'app-ordering',
standalone: true,
template: `
<ng-template #row let-label>
<li>{{ label }}</li>
</ng-template>
<ul>
<ng-container #host />
</ul>
<button (click)="addEnd()">Append</button>
<button (click)="addStart()">Prepend</button>
<button (click)="moveFirstToLast()">Erste → Ende</button>
`,
})
export class OrderingComponent {
tpl = viewChild.required<TemplateRef<{ $implicit: string }>>('row');
host = viewChild.required('host', { read: ViewContainerRef });
private counter = 0;
addEnd() {
this.host().createEmbeddedView(this.tpl(), {
$implicit: `Item ${++this.counter}`,
});
}
addStart() {
this.host().createEmbeddedView(
this.tpl(),
{ $implicit: `Item ${++this.counter}` },
{ index: 0 }
);
}
moveFirstToLast() {
const vcr = this.host();
if (vcr.length < 2) return;
const first = vcr.get(0) as EmbeddedViewRef<unknown>;
vcr.move(first, vcr.length - 1);
}
}Beachte den dritten Parameter von createEmbeddedView(): ein Options-Objekt mit index (Position) und optional injector. move() und insert() arbeiten mit bereits existierenden ViewRef-Instanzen — sie zerstören und rebuilden nichts, sondern reordern nur. Das ist deutlich günstiger als ein clear() + Recreate.
Eigenes *appWhen mit else-Branch
Strukturdirektiven sind nichts anderes als kleine Konsumenten von TemplateRef und ViewContainerRef. Hier ist ein *appWhen="cond; else other", das genau wie *ngIf mit Else-Branch funktioniert — der Mechanismus dahinter wird sichtbar.
import {
Directive,
Input,
TemplateRef,
ViewContainerRef,
inject,
} from '@angular/core';
@Directive({
selector: '[appWhen]',
standalone: true,
})
export class WhenDirective {
private vcr = inject(ViewContainerRef);
private thenTpl = inject(TemplateRef);
private elseTpl: TemplateRef<unknown> | null = null;
private current: 'then' | 'else' | null = null;
// Microsyntax: *appWhen="value; else otherTpl"
@Input() set appWhen(value: unknown) {
this.update(!!value);
}
@Input() set appWhenElse(tpl: TemplateRef<unknown> | null) {
this.elseTpl = tpl;
this.update(this.current === 'then');
}
private update(showThen: boolean) {
const target = showThen ? 'then' : 'else';
if (target === this.current) return;
this.vcr.clear();
if (showThen) {
this.vcr.createEmbeddedView(this.thenTpl);
this.current = 'then';
} else if (this.elseTpl) {
this.vcr.createEmbeddedView(this.elseTpl);
this.current = 'else';
} else {
this.current = null;
}
}
}<p *appWhen="user(); else anon">Hallo, {{ user()?.name }}!</p>
<ng-template #anon><p>Bitte einloggen.</p></ng-template>Drei Punkte machen das Pattern aus: Die Directive injectet ihren eigenen TemplateRef (das kompilierte Markup hinter *appWhen) und einen ViewContainerRef (den Anker, der vom Stern-Compiler erzeugt wird). Die Microsyntax-Konvention ; else otherTpl setzt automatisch ein zweites Input appWhenElse zusammen — Angular erkennt das Camel-Case-Schema. Beim Wechsel der Bedingung wird die alte View zerstört und die neue eingehängt.
Pro Item-Typ ein Template
Wenn eine Liste verschiedene Typen von Einträgen anzeigt (Activity-Feed, Inbox mit Notifications und Mails, Suchergebnisse aus mehreren Quellen), ist die saubere Lösung: ein Template pro Typ und NgTemplateOutlet als Dispatcher.
import { Component, signal, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
type FeedItem =
| { kind: 'message'; id: string; from: string; text: string }
| { kind: 'like'; id: string; user: string; postId: string }
| { kind: 'follow'; id: string; user: string };
@Component({
selector: 'app-feed',
standalone: true,
imports: [NgTemplateOutlet],
template: `
<ng-template #message let-item>
<article>
<strong>{{ item.from }}</strong>: {{ item.text }}
</article>
</ng-template>
<ng-template #like let-item>
<article>{{ item.user }} liked post {{ item.postId }}</article>
</ng-template>
<ng-template #follow let-item>
<article>{{ item.user }} folgt dir</article>
</ng-template>
@for (item of items(); track item.id) {
<ng-container
*ngTemplateOutlet="
templateFor(item);
context: { $implicit: item }
"
/>
}
`,
})
export class FeedComponent {
// viewChild() für jeden Template-Slot
// (kürzer mit map; hier ausgeschrieben für Klarheit)
items = signal<FeedItem[]>([
{ kind: 'message', id: '1', from: 'Anna', text: 'Hallo!' },
{ kind: 'like', id: '2', user: 'Bernd', postId: 'p7' },
{ kind: 'follow', id: '3', user: 'Carla' },
]);
// In der Praxis: Map<kind, TemplateRef> aufbauen
templateFor(item: FeedItem): TemplateRef<unknown> {
// gefüllt nach viewChild()-Resolution
return null!;
}
}Im echten Code holst du die drei TemplateRefs per viewChild.required<TemplateRef<unknown>>('message') etc. und baust eine Map Record<FeedItem['kind'], TemplateRef<unknown>>. Neue Item-Typen kosten dann nur ein neues <ng-template> plus einen Map-Eintrag — keine @if-Wand, kein Switch im TypeScript.
Wer erzeugt, der zerstört
Das Strukturdirektiven-Idiom (*ngIf, *ngFor, @if, @for) räumt automatisch auf — wechselt die Bedingung oder verschwindet ein Item, zerstört Angular die zugehörige Embedded-View. Sobald du selbst createEmbeddedView() (oder createComponent()) aufrufst, übernimmst du die Verantwortung.
import {
Component,
TemplateRef,
ViewContainerRef,
viewChild,
EmbeddedViewRef,
DestroyRef,
inject,
} from '@angular/core';
@Component({
selector: 'app-cleanup',
standalone: true,
template: `
<ng-template #tpl><p>Ich bin temporär.</p></ng-template>
<ng-container #host />
<button (click)="open()">Open</button>
`,
})
export class CleanupComponent {
tpl = viewChild.required<TemplateRef<unknown>>('tpl');
host = viewChild.required('host', { read: ViewContainerRef });
private views = new Set<EmbeddedViewRef<unknown>>();
constructor() {
// Beim Component-Destroy alle offenen Views zerstören
inject(DestroyRef).onDestroy(() => {
for (const v of this.views) v.destroy();
this.views.clear();
});
}
open() {
const ref = this.host().createEmbeddedView(this.tpl());
this.views.add(ref);
// Auch beim individuellen Destroy aus dem Set entfernen
ref.onDestroy(() => this.views.delete(ref));
}
}Wenn du den ganzen ViewContainer auf einen Schlag leeren willst, ist vcr.clear() der schnelle Weg — er zerstört alle Kinder-Views inklusive ihrer Bindings und DOM-Knoten. Für selektives Aufräumen ist view.destroy() pro Instanz das richtige Mittel.
Pitfalls
ng-template ohne Insertion-Point bleibt unsichtbar
Ein nacktes <ng-template>…</ng-template> rendert nichts — der Compiler übersetzt es in eine Schablone, kein DOM. Erst eine Strukturdirektive, ein *ngTemplateOutlet oder ein expliziter createEmbeddedView()-Aufruf macht den Inhalt sichtbar. Anfänger setzen dort gern Markup hinein und wundern sich, warum nichts erscheint.
viewChild() ist sauberer als @ViewChild
Die Signal-Variante viewChild<TemplateRef<…>>(‘foo’) integriert sich nahtlos mit effect() und computed(), ohne Timing-Krampf in ngAfterViewInit. Der initiale Wert ist undefined, danach ein gefülltes TemplateRef — das zwingt zu sauberen Null-Checks und beugt Subtle-Bugs vor.
createEmbeddedView ohne destroy() leakt Views
Solange der ViewContainer lebt (typisch: bis die Host-Component zerstört wird), bleibt jede manuell eingehängte View im Memory hängen. Bei Listen mit dynamisch gesetzten Inhalten unbedingt einen DestroyRef.onDestroy-Hook setzen oder bei jedem Update den alten EmbeddedViewRef zerstören, bevor ein neuer erzeugt wird.
Das Context-Object ist NICHT reaktiv
Setze ein Property direkt auf ref.context.foo = … — das Template sieht die Änderung erst nach ref.markForCheck() (OnPush) oder ref.detectChanges() (detached). Saubere Alternative: die View destroyen und mit neuem Context neu erzeugen, oder Signals im Context verwenden, die selbst Change Detection triggern.
$implicit ist die Default-let-Variable
Eine Deklaration let-foo ohne =… greift automatisch auf context.$implicit zu. let-foo=‘name’ hingegen liest context.name. Zwei häufige Fehler: let-foo wird mit dem Property-Key foo verwechselt, oder das Context-Object liefert keinen $implicit-Slot, obwohl im Template darauf zugegriffen wird.
ViewContainerRef im Constructor zeigt auf den eigenen Host
inject(ViewContainerRef) in einer Component liefert einen Container, dessen Anker das Component-Tag selbst ist — neue Views landen damit als Geschwister, nicht als Kinder. Wer Inhalt INNERHALB der eigenen Component einfügen will, braucht ein dediziertes <ng-container #host /> plus viewChild(‘host’, { read: ViewContainerRef }).
@ViewChild ohne read: liefert die Component, nicht den Container
Steht @ViewChild(‘host’) auf einem Component-Tag, ist das Resultat die Component-Instanz. Den ViewContainer holst du nur mit explizitem DI-Token: @ViewChild(‘host’, { read: ViewContainerRef }). Der gleiche Trick funktioniert auch für ElementRef oder TemplateRef, je nachdem, was an der Stelle steht.
NgTemplateOutletContext typsicher generisch nutzen
NgTemplateOutlet<C> ist generisch. Du kannst dir den Context-Typ in der Template-Bindung erzwingen: [ngTemplateOutletContext]=“ctx” mit ctx: MyCtx = { $implicit: user, role: ‘admin’ } in der TS-Datei. So fliegt ein Tippfehler im Property-Namen zur Compile-Zeit auf, statt erst zur Laufzeit als undefined-Bug im Template aufzutauchen.
Weiterführende Ressourcen
Externe Quellen
- ViewContainerRef API – Angular.dev
- TemplateRef API – Angular.dev
- EmbeddedViewRef API – Angular.dev
- NgTemplateOutlet API – Angular.dev
- createComponent() API – Angular.dev