À propos de ce cours
viewChild() cherche un composant ou un élément dans le template et renvoie un signal contenant le résultat. viewChild.required() garantit sa présence. Le localisateur est une classe ou une variable de référence, et l’option read choisit ce qu’on récupère, par exemple un ElementRef pour agir sur le DOM. viewChildren() renvoie tous les enfants, contentChildren() le contenu projeté. Les résultats ne sont disponibles qu’après le premier affichage (afterNextRender, computed, effect). Le cours montre quand préférer une variable #ref ou les couples input / output, présente l’ancienne écriture @ViewChild() avec sa migration, puis se termine par 5 activités corrigées et les erreurs fréquentes.
-
Objectifs
- Obtenir l'instance d'un composant enfant avec
viewChild()etviewChild.required(), puis appeler ses méthodes et lire ses signaux - Choisir entre une requête et une simple variable de référence
#ref - Accéder à un élément du DOM avec
ElementRefet l'optionread - Gérer un enfant qui apparaît et disparaît (
@if) aveccomputed()eteffect() - Interroger plusieurs enfants avec
viewChildren()et le contenu projeté aveccontentChildren() - Savoir quand le résultat d'une requête est disponible (
afterNextRender) - Lire et migrer l'ancienne écriture
@ViewChild()/@ViewChildren()+ngAfterViewInit
- Obtenir l'instance d'un composant enfant avec
-
Présentation
- Avec les entrées et les sorties, parent et enfant communiquent sans se connaître : le parent envoie des données, l'enfant annonce des événements.
- Parfois, le parent doit commander l'enfant : passer à la diapositive suivante, ouvrir une boîte de dialogue, donner le focus à un champ, corriger toutes les questions d'un quiz. Il lui faut alors une référence directe vers l'enfant.
- C'est le rôle des requêtes de vue:
viewChild()cherche un élément dans le template du composant et renvoie un signal qui contient le résultat. Elles remplacent les décorateurs@ViewChild()et@ViewChildren().

- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/requetes/. Tous les exemples ont été testés avec Angular 22 (composantsOnPushpar défaut, sans Zone.js).
-
Obtenir un composant enfant avec
viewChild()- On passe à
viewChild()un localisateur: ici la classe du composant recherché. Angular cherche dans le template le premier élément correspondant. viewChild.required()indique que l'enfant est toujours présent : le type du résultat n'inclut pasundefined.- Le résultat est un signal :
this.diaporama()renvoie l'instance, dont on utilise les membres publics.
- On passe à
import { Component, computed, signal, viewChild } from '@angular/core';
// Composant enfant : un diaporama qui expose des méthodes et un signal public
@Component({
selector: 'app-diaporama',
template: `<div class="diapo">{{ diapos[index()] }}</div>`,
styles: `.diapo { width: 260px; padding: 18px; text-align: center; background: #e8f1fb; border: 1px solid #3178c6; border-radius: 8px; font-size: 18px; }`,
})
export class Diaporama {
readonly diapos = ['Bienvenue', 'Les composants', 'Les signals', 'Merci !'];
readonly index = signal(0); // public : le parent peut le LIRE
suivant(): void {
this.index.update((i) => Math.min(i + 1, this.diapos.length - 1));
}
precedent(): void {
this.index.update((i) => Math.max(i - 1, 0));
}
aller(i: number): void {
this.index.set(i);
}
}
// Composant parent : il pilote l'enfant grâce à viewChild()
@Component({
selector: 'app-premiere-requete',
imports: [Diaporama],
template: `
<app-diaporama />
<button (click)="diaporama().precedent()">‹ Précédente</button>
<button (click)="diaporama().suivant()">Suivante ›</button>
<button (click)="diaporama().aller(0)">Début</button>
<p>{{ position() }}</p>
`,
})
export class PremiereRequete {
// On cherche dans la vue le premier composant Diaporama
protected readonly diaporama = viewChild.required(Diaporama);
// Le résultat est un signal : on peut l'utiliser dans un computed
protected readonly position = computed(() => `Diapo ${this.diaporama().index() + 1} / ${this.diaporama().diapos.length}`);
}
Bienvenue Diapo 1 / 4
(Suivante ×2) Les signals Diapo 3 / 4
(Suivante ×3) Merci ! Diapo 4 / 4 ← bloqué sur la dernière
(‹ Précédente) Les signals Diapo 3 / 4
(Début) Bienvenue Diapo 1 / 4
positionest uncomputedqui lit deux signaux : la requêtediaporama()et le signal public de l'enfantindex(). Il se met à jour à chaque changement de diapositive.- Seuls les membres publics sont accessibles. Si
suivant()étaitprotected:TS2445: Property 'suivant' is protected and only accessible within class 'Diaporama' and its subclasses. - L'enfant garde le contrôle de ses données : le parent appelle
suivant(), c'est l'enfant qui vérifie les limites.
-
Variable de référence ou requête ?
- Si l'appel se fait uniquement dans le template, une variable de référence (voir « Les variables de template dans Angular ») suffit : pas besoin de requête.
import { Component } from '@angular/core';
import { Diaporama } from './premiere-requete';
// Pour un simple appel depuis le template, une variable de référence suffit
@Component({
selector: 'app-variable-ou-requete',
imports: [Diaporama],
template: `
<app-diaporama #diapo />
<button (click)="diapo.suivant()">Suivante ›</button>
<span>Diapo {{ diapo.index() + 1 }}</span>
`,
})
export class VariableOuRequete {}
Bienvenue Diapo 1
(Suivante ›) Les composants Diapo 2
#diapoplacée sur un composant désigne l'instance du composant (et non la balise HTML).- La requête devient nécessaire dès qu'on a besoin de l'enfant dans la classe: dans un
computed, uneffect, une méthode qui fait plusieurs choses.

-
Accéder à un élément du DOM
- Le localisateur peut aussi être le nom d'une variable de référence, écrit entre guillemets :
viewChild('champ'). Sur une balise HTML, le résultat est unElementRef, dont la propriéténativeElementest l'élément du DOM. - On précise le type pour profiter de l'autocomplétion :
viewChild.required<ElementRef<HTMLInputElement>>('champ'). - L'option
readchoisit ce que l'on récupère :viewChild(Diaporama, { read: ElementRef })donne la balise hôte<app-diaporama>au lieu de l'instance de la classe.
- Le localisateur peut aussi être le nom d'une variable de référence, écrit entre guillemets :
import { Component, ElementRef, afterNextRender, signal, viewChild } from '@angular/core';
import { Diaporama } from './premiere-requete';
@Component({
selector: 'app-element-dom',
imports: [Diaporama],
template: `
<input #champ placeholder="Votre prénom" value="Amira" />
<button (click)="focaliser()">Sélectionner le texte</button>
<canvas #toile width="220" height="40"></canvas>
<app-diaporama #diapo />
<p>{{ infos() }}</p>
`,
styles: `canvas { display: block; margin: 8px 0; border: 1px solid #cbd5e1; }`,
})
export class ElementDom {
// 1. Par variable de référence : on obtient un ElementRef sur la balise
private readonly champ = viewChild.required<ElementRef<HTMLInputElement>>('champ');
private readonly toile = viewChild.required<ElementRef<HTMLCanvasElement>>('toile');
// 2. Option read : l'élément hôte d'un composant, au lieu de l'instance de sa classe
private readonly hoteDiapo = viewChild.required(Diaporama, { read: ElementRef });
protected readonly infos = signal('');
constructor() {
// Le DOM n'existe qu'après le premier affichage
afterNextRender(() => {
this.champ().nativeElement.focus();
const ctx = this.toile().nativeElement.getContext('2d')!;
ctx.fillStyle = '#dd0031';
ctx.fillRect(10, 10, 120, 20);
const hote = this.hoteDiapo().nativeElement as HTMLElement;
this.infos.set(`focus : ${document.activeElement?.tagName} · hôte : <${hote.tagName.toLowerCase()}>`);
});
}
protected focaliser(): void {
this.champ().nativeElement.select();
}
}
(au chargement) le curseur est dans le champ, un rectangle rouge est dessiné sur le canvas
focus : INPUT · hôte : <app-diaporama>
(Sélectionner le texte) « Amira » est sélectionné
- Le DOM n'existe qu'après le premier affichage : on y touche dans
afterNextRender(), appelé une fois, juste après le rendu. - Sans type,
viewChild('champ')renvoie un résultat de type inconnu :this.champ()?.nativeElementest refusé avecTS2339: Property 'nativeElement' does not exist on type '{}'. - Manipulez le DOM avec modération : pour afficher, masquer ou styliser, les liaisons du template (
@if,[class],[style]) restent la bonne solution.ElementRefsert à ce que le template ne sait pas faire :focus(),select(), dessiner sur un<canvas>,showModal()… - Attention : une faute de frappe dans le nom (
'chmp') n'est pas détectée à la compilation. Avec.required, la lecture échoue à l'exécution :NG0951: Child query result is required but no value is available.

-
Un enfant qui apparaît et disparaît
- Un enfant placé dans un
@ifn'existe pas toujours. On utilise alorsviewChild()sans.required: le résultat vautundefinedtant que l'enfant est absent. - Comme la requête est un signal, un
computedou uneffectqui la lit est relancé chaque fois que l'enfant apparaît ou disparaît.
- Un enfant placé dans un
import { Component, computed, effect, input, signal, viewChild } from '@angular/core';
@Component({
selector: 'app-panneau-details',
template: `<div class="panneau"><strong>{{ titre() }}</strong> <button (click)="plie.set(!plie())">{{ plie() ? '+' : '−' }}</button>
@if (!plie()) { <p>Durée : 3 h · niveau débutant</p> }</div>`,
styles: `.panneau { border: 1px solid #cbd5e1; padding: 6px 10px; border-radius: 6px; max-width: 300px; }`,
})
export class PanneauDetails {
readonly titre = input('Détails');
readonly plie = signal(false);
}
@Component({
selector: 'app-enfant-conditionnel',
imports: [PanneauDetails],
template: `
<button (click)="visible.set(!visible())">{{ visible() ? 'Masquer' : 'Afficher' }} le panneau</button>
@if (visible()) { <app-panneau-details titre="Angular débutant" /> }
<p>{{ etat() }}</p>
<p>Journal : {{ journal().join(' → ') }}</p>
`,
})
export class EnfantConditionnel {
protected readonly visible = signal(false);
// Sans .required : le résultat vaut undefined tant que l'enfant n'existe pas
private readonly panneau = viewChild(PanneauDetails);
protected readonly etat = computed(() => {
const p = this.panneau();
return p ? `Panneau « ${p.titre()} » ${p.plie() ? 'plié' : 'ouvert'}` : 'Aucun panneau';
});
protected readonly journal = signal<string[]>([]);
constructor() {
// L'effet est relancé chaque fois que l'enfant apparaît ou disparaît
effect(() => {
const present = this.panneau() !== undefined;
this.journal.update((j) => [...j, present ? 'créé' : 'absent']);
});
}
}
Aucun panneau Journal : absent
(Afficher le panneau) Panneau « Angular débutant » ouvert Journal : absent → créé
(clic sur −) Panneau « Angular débutant » plié
(Masquer le panneau) Aucun panneau Journal : absent → créé → absent
- Le type est
PanneauDetails | undefined: écrirethis.panneau().titre()est refusé (TS2532: Object is possibly 'undefined'.). On teste la valeur, ou on utilise?.. - Le
computedlit aussi les signaux de l'enfant (plie()) : il suit l'état interne du panneau. - L'
effectne lit que la requête : il ne se relance que lorsque l'enfant est créé ou détruit, pas quand on le plie.
-
Plusieurs enfants :
viewChildren()viewChildren()renvoie tous les éléments correspondants, sous la forme d'un signal de tableau en lecture seule (Signal<readonly CaseOption[]>), dans l'ordre d'affichage.- Le tableau est mis à jour automatiquement quand un
@forajoute ou retire des enfants.
import { Component, computed, input, signal, viewChildren } from '@angular/core';
@Component({
selector: 'app-case-option',
template: `<label><input type="checkbox" [checked]="coche()" (change)="coche.set(!coche())" /> {{ libelle() }}</label>`,
styles: `:host { display: block; }`,
})
export class CaseOption {
readonly libelle = input.required<string>();
readonly coche = signal(false);
definir(valeur: boolean): void {
this.coche.set(valeur);
}
}
@Component({
selector: 'app-plusieurs-enfants',
imports: [CaseOption],
template: `
@for (o of options(); track o) { <app-case-option [libelle]="o" /> }
<button (click)="toutes(true)">Tout cocher</button>
<button (click)="toutes(false)">Tout décocher</button>
<button (click)="options.update((l) => [...l, 'Option ' + (l.length + 1)])">Ajouter</button>
<p>{{ resume() }}</p>
`,
})
export class PlusieursEnfants {
protected readonly options = signal(['Wi-Fi', 'Parking', 'Petit-déjeuner']);
// Toutes les cases de la vue, dans l'ordre d'affichage : Signal<readonly CaseOption[]>
private readonly cases = viewChildren(CaseOption);
protected readonly resume = computed(() => {
const cochees = this.cases().filter((c) => c.coche()).map((c) => c.libelle());
return `${cochees.length} / ${this.cases().length} cochée(s) : ${cochees.join(', ') || 'aucune'}`;
});
protected toutes(valeur: boolean): void {
this.cases().forEach((c) => c.definir(valeur));
}
}
0 / 3 cochée(s) : aucune
(case Parking) 1 / 3 cochée(s) : Parking
(Tout cocher) 3 / 3 cochée(s) : Wi-Fi, Parking, Petit-déjeuner
(Ajouter) 3 / 4 cochée(s) : Wi-Fi, Parking, Petit-déjeuner ← Option 4 détectée
(Tout décocher) 0 / 4 cochée(s) : aucune
toutes()appelle une méthode sur chaque enfant : c'est le cas typique où une requête est plus simple que des entrées et des sorties.- Le
computedlit la liste et le signalcoche()de chaque case : il réagit à l'ajout d'une case comme à un clic. - Le résultat est un vrai tableau :
filter,map,[0],length. Les méthodes de l'ancienneQueryListn'existent plus :this.cases().firstdonneTS2339: Property 'first' does not exist on type 'readonly CaseOption[]'.
-
Le contenu projeté :
contentChild()etcontentChildren()- Les requêtes de vue ne voient que ce que le composant écrit dans son propre template. Les éléments qu'un parent place entre ses balises et qu'il affiche avec
<ng-content />forment son contenu: on les interroge aveccontentChild()etcontentChildren(), qui s'utilisent de la même façon.
- Les requêtes de vue ne voient que ce que le composant écrit dans son propre template. Les éléments qu'un parent place entre ses balises et qu'il affiche avec
import { Component, afterNextRender, computed, contentChildren, input, signal } from '@angular/core';
// Un onglet : il ne s'affiche que s'il est actif
@Component({
selector: 'app-onglet',
template: `@if (actif()) { <div class="contenu"><ng-content /></div> }`,
styles: `.contenu { border: 1px solid #cbd5e1; padding: 8px; border-radius: 0 6px 6px 6px; }`,
})
export class Onglet {
readonly titre = input.required<string>();
readonly actif = signal(false);
}
// Le conteneur ne crée pas les onglets : il les REÇOIT par projection
@Component({
selector: 'app-onglets',
template: `
<nav>
@for (o of onglets(); track o; let i = $index) {
<button [class.actif]="o.actif()" (click)="choisir(i)">{{ o.titre() }}</button>
}
</nav>
<ng-content />
`,
styles: `button { border: 1px solid #cbd5e1; background: #f8fafc; color: #1f2937; padding: 4px 10px; margin-right: 2px; } .actif { background: #3178c6; color: #fff; }`,
})
export class Onglets {
// contentChildren : les composants placés ENTRE <app-onglets> et </app-onglets>
protected readonly onglets = contentChildren(Onglet);
readonly nombre = computed(() => this.onglets().length);
constructor() {
afterNextRender(() => this.choisir(0)); // le premier onglet est actif au départ
}
choisir(index: number): void {
this.onglets().forEach((o, i) => o.actif.set(i === index));
}
}
@Component({
selector: 'app-contenu-projete',
imports: [Onglets, Onglet],
template: `
<app-onglets #groupe>
<app-onglet titre="Description">Un clavier mécanique silencieux.</app-onglet>
<app-onglet titre="Caractéristiques">105 touches, USB-C, rétroéclairé.</app-onglet>
<app-onglet titre="Avis">4,5 / 5 sur 38 avis.</app-onglet>
</app-onglets>
<p>{{ groupe.nombre() }} onglets reçus par projection</p>
`,
})
export class ContenuProjete {}
[Description] Caractéristiques Avis Un clavier mécanique silencieux.
3 onglets reçus par projection
(Caractéristiques) Description [Caractéristiques] Avis 105 touches, USB-C, rétroéclairé.

- Le composant
Ongletsne sait pas combien d'onglets il recevra :contentChildren(Onglet)les lui fournit, et il fabrique les boutons à partir de leurs titres. - Avec
viewChildren(Onglet)à la place, le tableau serait vide (testé : 0 élément), sans aucune erreur : les onglets n'appartiennent pas à la vue d'Onglets. - Le parent lit le nombre d'onglets avec une variable de référence (
#groupe) : seulnombre, public, est accessible.
-
Quand le résultat est-il disponible ?
- Dans le constructeur, la vue n'est pas encore créée : la requête vaut
undefined. Les résultats sont disponibles après le premier affichage.
- Dans le constructeur, la vue n'est pas encore créée : la requête vaut
import { Component, afterNextRender, signal, viewChild } from '@angular/core';
import { Diaporama } from './premiere-requete';
// Quand le résultat d'une requête est-il disponible ?
@Component({
selector: 'app-trop-tot',
imports: [Diaporama],
template: `<app-diaporama /> <p>{{ journal().join(' · ') }}</p>`,
})
export class TropTot {
private readonly diaporama = viewChild(Diaporama);
protected readonly journal = signal<string[]>([]);
constructor() {
this.noter('constructeur', this.diaporama()); // trop tôt : la vue n'est pas encore créée
afterNextRender(() => {
this.noter('afterNextRender', this.diaporama()); // la vue est affichée
this.diaporama()?.aller(2);
});
}
private noter(moment: string, d: Diaporama | undefined): void {
this.journal.update((j) => [...j, `${moment} : ${d ? 'Diaporama trouvé' : 'undefined'}`]);
}
}
Les signals
constructeur : undefined · afterNextRender : Diaporama trouvé

- Avec
viewChild.required, lire la requête dans le constructeur est refusé dès la compilation :NG8118: `diaporama` is a required `viewChild` and does not have a value in this context. - Dans un gestionnaire d'événement (clic) ou dans un
computedlu par le template, la question ne se pose pas : la vue existe déjà. - Comme
output()etinput(),viewChild()ne s'appelle que dans l'initialisation d'une propriété :NG8110: Unsupported call to the viewChild function. This function can only be called in the initializer of a class member.
-
L'ancienne écriture :
@ViewChild()et@ViewChildren()- Avant les signals, une requête était une propriété décorée par
@ViewChild()ou@ViewChildren(). Le résultat n'était garanti qu'à partir du crochetngAfterViewInit. @ViewChildren()renvoie uneQueryList, qui prévient de ses changements par unObservable(changes). L'optionstatic: truerend le résultat disponible plus tôt (dèsngOnInit) pour un élément qui n'est pas dans un@ifou un@for.
- Avant les signals, une requête était une propriété décorée par
import { AfterViewInit, Component, ElementRef, QueryList, ViewChild, ViewChildren, signal } from '@angular/core';
import { CaseOption } from './plusieurs-enfants';
import { Diaporama } from './premiere-requete';
// L'ancienne écriture : décorateurs @ViewChild / @ViewChildren et ngAfterViewInit
@Component({
selector: 'app-ancienne-ecriture',
imports: [Diaporama, CaseOption],
template: `
<input #champ value="ancien" />
<app-diaporama />
@for (o of ['A', 'B']; track o) { <app-case-option [libelle]="o" /> }
<button (click)="diaporama.suivant()">Suivante ›</button>
<p>{{ message() }}</p>
`,
})
export class AncienneEcriture implements AfterViewInit {
@ViewChild(Diaporama) diaporama!: Diaporama;
@ViewChild('champ', { static: true }) champ!: ElementRef<HTMLInputElement>;
@ViewChildren(CaseOption) cases!: QueryList<CaseOption>;
protected readonly message = signal('');
ngAfterViewInit(): void {
// Les résultats ne sont garantis qu'à partir de ce crochet
this.cases.first.definir(true);
this.message.set(`${this.cases.length} cases · champ : ${this.champ.nativeElement.value} · diapo : ${this.diaporama.index() + 1}`);
}
}
2 cases · champ : ancien · diapo : 1 (la case A est cochée)
(Suivante ›) Les composants

- Le
!dediaporama!: Diaporamapromet à TypeScript que la propriété sera remplie, sans rien garantir. Lue trop tôt, par exemple dansngOnInit, elle provoque à l'exécution :TypeError: Cannot read properties of undefined (reading 'suivant'). - Avec
viewChild(), le type dit la vérité (T | undefined), le compilateur vérifie les lectures trop précoces, etcomputedremplacengAfterViewInit+changes.subscribe. - Angular fournit une migration automatique :
ng generate @angular/core:signal-queries-migration.
-
Aide-mémoire

-
Activité 1 : le focus sur le premier champ en erreur
- Créer
app-formulaire-focus: trois champs (nom, e-mail, âge) marqués par la même variable#champet identifiés par leur attributname, et un bouton « S'inscrire ». - Un
computedcalcule la liste des erreurs : nom d'au moins 2 lettres, e-mail valide, âge entre 16 et 99. - À l'envoi, s'il y a des erreurs : les afficher, encadrer les champs fautifs en rouge et placer le curseur dans le premier champ en erreur, grâce à
viewChildren<ElementRef<HTMLInputElement>>('champ'). Sinon, afficher « Bienvenue Sami ! ».
- Créer

- Vérifier : formulaire vide → curseur dans « Nom » ; nom rempli → curseur dans « E-mail » ; e-mail correct et âge 12 → curseur dans « Âge ».
- Pourquoi utiliser
viewChildrenavec une variable, plutôt que troisviewChild?
-
Activité 2 : un tableau de dessin
- Créer
app-tableau-dessin: un<canvas>de 480 × 200, cinq pastilles de couleur, un curseur d'épaisseur (1 à 12) et un bouton « Effacer ». - Récupérer le canvas avec
viewChild.required<ElementRef<HTMLCanvasElement>>('toile')et son contexte 2D dansafterNextRender. - Dessiner avec les événements
pointerdown,pointermove,pointerupetpointerleave(qui fonctionnent à la souris comme au doigt), en convertissant la position du pointeur dans les coordonnées du canvas. Afficher le nombre de traits ; « Effacer » vide le canvas et remet le compteur à 0.
- Créer

- Pourquoi le contexte ne peut-il pas être récupéré dans le constructeur ?
- Ajouter un bouton « Télécharger » qui utilise
toDataURL('image/png')sur le canvas.
-
Activité 3 : un quiz qui se corrige
- Créer
app-question: entrée obligatoirequestion(énoncé, choix, indice de la bonne réponse), entréenumero, signaux publicschoisietcorrigee,computedpublicjuste, méthodescorriger()etreinitialiser(). Une fois corrigée, la bonne réponse est surlignée en vert et la mauvaise réponse choisie est barrée en rouge. - Le parent
app-quizaffiche trois questions et récupère les trois composants avecviewChildren(Question). Uncomputedaffiche « 2 / 3 question(s) répondue(s) », puis « Score : 2 / 3 » après « Corriger » (qui appellecorriger()sur chaque question). « Recommencer » réinitialise tout.
- Créer

- Le parent ne stocke aucune réponse : où sont-elles ? Quel est l'avantage, et quel est l'inconvénient ?
- Ajouter une quatrième question : faut-il modifier le calcul du score ?
-
Activité 4 : une boîte modale avec
<dialog>- Créer
app-modaleautour de la balise native<dialog>: entréetitre, contenu projeté (<ng-content />), signal publicouverte, sortiefermee, et deux méthodes publiquesouvrir()(showModal()) etfermer()(close()), obtenues parviewChildsur la balise. - L'événement
closedu<dialog>remetouverteàfalseet émetfermee: il se déclenche aussi avec la touche Échap. - Le parent
app-page-inscriptionobtient la modale parviewChild.required(Modale). Le bouton « Lire les conditions » l'ouvre ; « J'accepte », placé dans le contenu projeté, coche la case et ferme la modale. Afficher « Modale ouverte · fermetures : 0 · conditions non acceptées ».
- Créer

- Pourquoi une entrée
[ouverte]ne suffirait-elle pas pour ouvrir un<dialog>en mode modal ? - Ouvrir puis fermer avec Échap, avec « Fermer » et avec « J'accepte » : le compteur de fermetures doit valoir 3.
-
Activité 5 : migrer
@ViewChild()versviewChild()- On vous fournit le composant suivant, qui affiche des étiquettes (
app-etiquette, avec un signal publicselectionnee) :
- On vous fournit le composant suivant, qui affiche des étiquettes (
export class EtiquettesAncien implements AfterViewInit {
readonly noms = input.required<string[]>();
@ViewChild('filtre', { static: true }) filtre!: ElementRef<HTMLInputElement>;
@ViewChildren(Etiquette) etiquettes!: QueryList<Etiquette>;
total = signal(0);
private readonly destroyRef = inject(DestroyRef);
ngAfterViewInit(): void {
this.filtre.nativeElement.placeholder = 'Filtrer (ancien)';
this.total.set(this.etiquettes.length);
const abo = this.etiquettes.changes.subscribe((l: QueryList<Etiquette>) => this.total.set(l.length));
this.destroyRef.onDestroy(() => abo.unsubscribe());
}
toutDeselectionner(): void {
this.etiquettes.forEach((e) => e.selectionnee.set(false));
}
}
- Écrire
app-etiquettes, son équivalent avecviewChild.required(),viewChildren(),afterNextRender()et descomputed. Ajouter le nombre d'étiquettes sélectionnées, que l'ancienne version ne sait pas suivre simplement. - Afficher les deux versions l'une sous l'autre, avec un bouton « Ajouter une étiquette » commun : les deux totaux doivent passer de 3 à 4.

- Combien de lignes disparaissent ? Pourquoi n'y a-t-il plus d'abonnement à gérer ?
- Lancer
ng generate @angular/core:signal-queries-migrationsur une copie deEtiquettesAncienet comparer avec votre version.
- Les corrigés des cinq activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier
corriges/).
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG0951: Child query result is required but no value is available. |
viewChild.required sans résultat : nom de variable erroné, élément dans un @if faux |
Corriger le localisateur ; sinon viewChild() sans .required |
NG8118: `diaporama` is a required `viewChild` and does not have a value in this context. |
Requête obligatoire lue dans le constructeur | La lire dans afterNextRender, un computed ou un gestionnaire |
NG8110: Unsupported call to the viewChild function. … |
viewChild() appelée dans une méthode |
La déclarer comme propriété de classe |
TS2532: Object is possibly 'undefined'. |
Requête facultative lue sans précaution | ?., un test, ou .required si l'enfant est toujours là |
TS2339: Property 'nativeElement' does not exist on type '{}'. |
Requête par nom sans type | viewChild<ElementRef<HTMLInputElement>>('champ') |
TS2339: Property 'first' does not exist on type 'readonly CaseOption[]'. |
Méthodes de QueryList sur un tableau |
[0], at(-1), length… |
TS2445: Property 'suivant' is protected … |
Membre non public de l'enfant | Rendre public ce que le parent doit appeler |
TypeError: Cannot read properties of undefined (reading 'suivant') |
@ViewChild lu dans ngOnInit |
ngAfterViewInit, ou migrer vers viewChild() |
viewChildren() renvoie un tableau vide |
Éléments projetés, pas dans la vue | contentChildren() |
| Le focus ou le dessin ne se fait pas au chargement | DOM manipulé avant le rendu | afterNextRender(() => …) |
| Le parent modifie directement les données de l'enfant | Requête utilisée pour transmettre des données | input() pour les données, requête pour les commandes |
-
À retenir
viewChild(Composant)renvoie un signal contenant l'instance de l'enfant (ouundefined) ;viewChild.required()garantit sa présence.- Le localisateur est une classe ou le nom d'une variable de référence ; sur une balise HTML on obtient un
ElementRef, et l'optionreadchoisit le résultat. viewChildren()renvoie un signal de tableau ;contentChild()/contentChildren()interrogent le contenu projeté.- Les résultats n'existent qu'après le premier affichage :
afterNextRenderpour le DOM,computedeteffectpour réagir. - Une variable
#refsuffit pour un appel depuis le template ; la requête sert dans la classe. - À réserver aux commandes (méthodes, DOM) : les données passent par
input()et les événements paroutput(). @ViewChild()+ngAfterViewInitest l'ancienne écriture : à connaître, à migrer versviewChild().
Une requête crée un lien fort entre le parent et l'enfant : utilisez-la quand il faut vraiment commander l'enfant, et laissez les entrées et les sorties faire le reste.