Les queries donnent accès à des éléments, composants ou directives du template. Depuis la v19, ce sont des Signals : viewChild() cherche dans le template du composant lui-même, contentChild() cherche dans le contenu projeté par le parent (ce qui est placé entre les balises du composant). Comme ce sont des Signals, vous pouvez les utiliser directement dans computed() et effect(), sans ngAfterViewInit et sans QueryList.changes.
| Cherche dans | Résultat | |
|---|---|---|
viewChild | le template du composant | Signal<T | undefined> |
viewChildren | le template du composant | Signal<readonly T[]> |
contentChild | le contenu projeté (ng-content) | Signal<T | undefined> |
contentChildren | le contenu projeté | Signal<readonly T[]> |
viewChild et 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 {
// par variable de référence du template
private readonly search = viewChild.required<ElementRef<HTMLInputElement>>('search');
// par le type du composant
private readonly results = viewChild(Results); // Signal<Results | undefined>
// toutes les correspondances
private readonly rows = viewChildren<ElementRef<HTMLLIElement>>('row'); // Signal<readonly ElementRef[]>
constructor() {
// ✗ Dans le constructeur, la vue n’existe pas encore : this.search() lèverait une erreur (required)
afterNextRender(() => this.search().nativeElement.focus()); // ✓ après le premier rendu
}
protected readonly rowCount = computed(() => this.rows().length); // ✓ réactif
}L’option read
// `read` choisit CE QUE vous obtenez de l’élément trouvé
readonly input = viewChild('field', { read: ElementRef }); // l’élément du DOM
readonly model = viewChild('field', { read: NgModel }); // une directive posée dessus
readonly host = viewChild('slot', { read: ViewContainerRef }); // pour des composants dynamiques
readonly tpl = viewChild('tpl', { read: TemplateRef }); // un <ng-template>
readonly inj = viewChild(Child, { read: Injector }); // v22.2 : l’injector de l’enfantcontentChildren : des composants qui collaborent
Le pattern classique : un conteneur (Tabs) et des enfants déclaratifs (Tab). Le parent les écrit en HTML, le conteneur les découvre avec contentChildren et décide comment les afficher.
// Tabs + Tab : le conteneur découvre les enfants placés par le PARENT
@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()]);
}Le nom, l’e-mail et l’avatar de l’utilisateur.
descendants et contentChild
// Par défaut, contentChildren ne regarde que les enfants DIRECTS du contenu projeté.
// Avec descendants: true, il cherche à toute profondeur (p. ex. dans un wrapper <div>).
readonly items = contentChildren(MenuItem, { descendants: true });
// contentChild pour un seul élément (p. ex. un template optionnel pour un en-tête)
readonly headerTpl = contentChild<TemplateRef<unknown>>('header');Avant : décorateurs et QueryList
// ✗ L’ancienne 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()); // abonnement manuel aux changements
}Pour le contenu reçu de l’extérieur, voir Projection de contenu ; pour travailler avec le DOM après le rendu, afterNextRender dans Lifecycle et DestroyRef.
Quand OUI, quand NON
- Focus, scroll, mesures du DOM (avec
afterNextRender). - Intégration avec des bibliothèques qui ont besoin d’un élément (graphiques, éditeurs).
- Composants composés : Tabs/Tab, Menu/MenuItem, Accordion/Panel.
- Obtenir un
ViewContainerRefpour des composants dynamiques.
- Pour lire/écrire l’état d’un enfant → utilisez input/output/model.
- Pour appeler des méthodes sur un enfant alors que cela peut être déclaratif (p. ex. « child.refresh() » à chaque changement) → passez un Input.
- Pour changer des classes/styles → des bindings dans le template.
Pièges fréquents
- Lire trop tôt : dans le constructeur, les queries n’ont pas de valeur ; un
.requiredlèveNG0951. - Un élément sous
@if:viewChildvautundefinedquand le bloc est masqué — n’y utilisez pas.required. - Les mesures du DOM dans
computed(comme dans la démo) fonctionnent, mais ne réagissent pas aux redimensionnements — pour cela, il faut unResizeObserver.
Aide-mémoire
viewChild('ref') // Signal<T | undefined>
viewChild.required('ref') // Signal<T> (lève une erreur si lu avant d’exister)
viewChild(MyComp) // par type
viewChildren('ref' | MyComp) // Signal<readonly T[]>
contentChild(...) / contentChildren(..., { descendants })
{ read: ElementRef | TemplateRef | ViewContainerRef | Directive | Injector }
// migration : ng generate @angular/core:signal-queries