Queries geben dir Zugriff auf Elemente, Komponenten oder Direktiven aus dem Template. Seit v19 sind sie Signals: viewChild() sucht im eigenen Template der Komponente, contentChild() sucht im projizierten Inhalt des Parents (was zwischen den Tags der Komponente steht). Weil sie Signals sind, kannst du sie direkt in computed() und effect() verwenden, ohne ngAfterViewInit und ohne QueryList.changes.
| Sucht in | Ergebnis | |
|---|---|---|
viewChild | dem Template der Komponente | Signal<T | undefined> |
viewChildren | dem Template der Komponente | Signal<readonly T[]> |
contentChild | dem projizierten Inhalt (ng-content) | Signal<T | undefined> |
contentChildren | dem projizierten Inhalt | Signal<readonly T[]> |
viewChild und viewChildren
import { Component, ElementRef, viewChild, viewChildren, afterNextRender } from '@angular/core';
@Component({
selector: 'app-search-box',
template: `
<input #search />
<app-results />
@for (item of items(); track item.id) { <li #row>{{ item.name }}</li> }
`,
})
export class SearchBox {
// über die Template-Referenzvariable
private readonly search = viewChild.required<ElementRef<HTMLInputElement>>('search');
// über den Typ der Komponente
private readonly results = viewChild(Results); // Signal<Results | undefined>
// alle Treffer
private readonly rows = viewChildren<ElementRef<HTMLLIElement>>('row'); // Signal<readonly ElementRef[]>
constructor() {
// ✗ Im Konstruktor existiert die View noch nicht: this.search() würde werfen (required)
afterNextRender(() => this.search().nativeElement.focus()); // ✓ nach dem ersten Rendern
}
protected readonly rowCount = computed(() => this.rows().length); // ✓ reaktiv
}Die Option read
// `read` bestimmt, WAS du vom gefundenen Element bekommst
readonly input = viewChild('field', { read: ElementRef }); // das DOM-Element
readonly model = viewChild('field', { read: NgModel }); // eine Direktive darauf
readonly host = viewChild('slot', { read: ViewContainerRef }); // für dynamische Komponenten
readonly tpl = viewChild('tpl', { read: TemplateRef }); // ein <ng-template>
readonly inj = viewChild(Child, { read: Injector }); // v22.2: der Injector des KindescontentChildren: Komponenten, die zusammenarbeiten
Das klassische Muster: ein Container (Tabs) und deklarative Kinder (Tab). Der Parent schreibt sie in HTML, der Container entdeckt sie mit contentChildren und entscheidet, wie er sie darstellt.
// Tabs + Tab: Der Container entdeckt die Kinder, die der PARENT platziert hat
@Component({
selector: 'app-tab',
template: `<ng-template #content><ng-content /></ng-template>`,
})
export class Tab {
readonly title = input.required<string>();
readonly content = viewChild.required<TemplateRef<unknown>>('content');
}
@Component({
selector: 'app-tabs',
imports: [NgTemplateOutlet],
template: `
@for (tab of tabs(); track tab; let i = $index) {
<button [class.active]="i === active()" (click)="active.set(i)">{{ tab.title() }}</button>
}
@if (current(); as tab) {
<ng-container [ngTemplateOutlet]="tab.content()" />
}
`,
})
export class Tabs {
protected readonly tabs = contentChildren(Tab); // Signal<readonly Tab[]>
protected readonly active = signal(0);
protected readonly current = computed(() => this.tabs()[this.active()]);
}Name, E-Mail und Avatar des Benutzers.
descendants und contentChild
// contentChildren sucht standardmäßig nur in den DIREKTEN Kindern des projizierten Inhalts.
// Mit descendants: true sucht es in beliebiger Tiefe (z. B. innerhalb eines <div>-Wrappers).
readonly items = contentChildren(MenuItem, { descendants: true });
// contentChild für ein einzelnes Element (z. B. ein optionales Template für einen Header)
readonly headerTpl = contentChild<TemplateRef<unknown>>('header');Früher: Decorators und QueryList
// ✗ Die alte API
@ViewChild('search') search!: ElementRef<HTMLInputElement>;
@ViewChildren(RowComponent) rows!: QueryList<RowComponent>;
@ContentChildren(Tab) tabs!: QueryList<Tab>;
ngAfterViewInit() {
this.search.nativeElement.focus();
this.rows.changes.subscribe(() => this.updateCount()); // manuelles Abonnieren der Änderungen
}Für Inhalte, die von außen kommen, siehe Content Projection; für DOM-Zugriff nach dem Rendern afterNextRender unter Lifecycle & DestroyRef.
Wann ja, wann nein
- Fokus, Scrollen, DOM-Messungen (mit
afterNextRender). - Integration mit Bibliotheken, die ein Element brauchen (Diagramme, Editoren).
- Zusammengesetzte Komponenten: Tabs/Tab, Menu/MenuItem, Accordion/Panel.
- Eine
ViewContainerReffür dynamische Komponenten holen.
- Um den Zustand eines Kindes zu lesen oder zu schreiben → nutze Input/Output/Model.
- Um Methoden eines Kindes aufzurufen, obwohl es auch deklarativ geht (z. B. „child.refresh()“ bei jeder Änderung) → übergib einen Input.
- Um Klassen/Styles zu ändern → Bindings im Template.
Häufige Stolperfallen
- Zu frühes Lesen: Im Konstruktor haben die Queries noch keinen Wert; ein
.requiredwirftNG0951. - Ein Element unter
@if:viewChildwirdundefined, wenn der Block ausgeblendet ist — verwende dort kein.required. - DOM-Messungen in
computed(wie in der Demo) funktionieren, sind aber nicht reaktiv auf Größenänderungen — dafür brauchst du einenResizeObserver.
Spickzettel
viewChild('ref') // Signal<T | undefined>
viewChild.required('ref') // Signal<T> (wirft, wenn vor dem Entstehen gelesen)
viewChild(MyComp) // nach Typ
viewChildren('ref' | MyComp) // Signal<readonly T[]>
contentChild(...) / contentChildren(..., { descendants })
{ read: ElementRef | TemplateRef | ViewContainerRef | Direktive | Injector }
// Migration: ng generate @angular/core:signal-queries