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.

HTML ng-template-basics.component.html
<!-- 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, &#123;&#123; name &#125;&#125;!</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-template setzt, greifen nirgends.
  • Eigener Binding-Scope. Variablen aus let-foo leben 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.

TypeScript template-ref-query.component.ts
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.

TypeScript viewcontainer-host.component.ts
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.

TypeScript template-context.component.ts
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.

TypeScript template-outlet.component.ts
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.

AspektcreateEmbeddedView(tpl)createComponent(Cmp)
QuelleTemplateRef<C>Type<T> (Component-Klasse)
RückgabeEmbeddedViewRef<C>ComponentRef<T>
Lifecycle-Hooksnur Template-Bindingsvolle Hooks: ngOnInit, ngOnDestroy, …
Inputs/OutputsContext-Object (let-)setInput(), instance.output.subscribe()
Eigenes Component-DInein, übernimmt vom Ankerja, eigene Injection-Hierarchie
Wiederverwendbarkeitleicht (gleicher Tpl überall)volle Komponenten-Logik
Typischer Use-CaseSlots, Renderer, Conditional WrapperModals, Tabs, Plugin-Slots
TypeScript embedded-vs-component.component.ts
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.

TypeScript embedded-view-lifecycle.component.ts
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:

  • context ist nicht reaktiv. Wenn du Felder direkt mutierst, muss markForCheck() (OnPush) oder detectChanges() (detached) folgen, sonst sieht das Template die Änderung nie.
  • rootNodes liefert 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.

TypeScript vcr-ordering.component.ts
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.

TypeScript when.directive.ts
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;
        }
    }
}
HTML when-usage.component.html
<p *appWhen="user(); else anon">Hallo, &#123;&#123; user()?.name &#125;&#125;!</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.

TypeScript polymorphic-feed.component.ts
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.

TypeScript cleanup-pattern.component.ts
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

/ Weiter

Zurück zu Komponenten-Interaktion

Zur Übersicht