Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Accéder à un composant enfant avec viewChild

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...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À 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.
  1. Objectifs

    • Obtenir l'instance d'un composant enfant avec viewChild() et viewChild.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 ElementRef et l'option read
    • Gérer un enfant qui apparaît et disparaît (@if) avec computed() et effect()
    • Interroger plusieurs enfants avec viewChildren() et le contenu projeté avec contentChildren()
    • Savoir quand le résultat d'une requête est disponible (afterNextRender)
    • Lire et migrer l'ancienne écriture @ViewChild() / @ViewChildren() + ngAfterViewInit
  1. 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().

viewChild() permet au parent d'obtenir l'instance d'un composant enfant

  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/requetes/. Tous les exemples ont été testés avec Angular 22 (composants OnPush par défaut, sans Zone.js).
  1. 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 pas undefined.
    • Le résultat est un signal : this.diaporama() renvoie l'instance, dont on utilise les membres publics.
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
  • position est un computed qui lit deux signaux : la requête diaporama() et le signal public de l'enfant index(). Il se met à jour à chaque changement de diapositive.
  • Seuls les membres publics sont accessibles. Si suivant() était protected: 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.
  1. 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
  • #diapo placé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, un effect, une méthode qui fait plusieurs choses.

Choisir entre input, output, variable de référence, viewChild et service

  1. 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 un ElementRef, dont la propriété nativeElement est l'élément du DOM.
    • On précise le type pour profiter de l'autocomplétion : viewChild.required<ElementRef<HTMLInputElement>>('champ').
    • L'option read choisit 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.
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()?.nativeElement est refusé avec TS2339: 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. ElementRef sert à 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.

Les localisateurs de viewChild() et ce qu'ils renvoient

  1. Un enfant qui apparaît et disparaît

    • Un enfant placé dans un @if n'existe pas toujours. On utilise alors viewChild() sans .required: le résultat vaut undefined tant que l'enfant est absent.
    • Comme la requête est un signal, un computed ou un effect qui la lit est relancé chaque fois que l'enfant apparaît ou disparaît.
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: écrire this.panneau().titre() est refusé (TS2532: Object is possibly 'undefined'.). On teste la valeur, ou on utilise ?..
  • Le computed lit aussi les signaux de l'enfant (plie()) : il suit l'état interne du panneau.
  • L'effect ne lit que la requête : il ne se relance que lorsque l'enfant est créé ou détruit, pas quand on le plie.
  1. 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 @for ajoute 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 computed lit la liste et le signal coche() 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'ancienne QueryList n'existent plus : this.cases().first donne TS2339: Property 'first' does not exist on type 'readonly CaseOption[]'.
  1. Le contenu projeté : contentChild() et contentChildren()

    • 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 avec contentChild() et contentChildren(), qui s'utilisent de la même façon.
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é.

viewChild interroge la vue, contentChild le contenu projeté

  • Le composant Onglets ne 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) : seul nombre, public, est accessible.
  1. 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.
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é

Quand le résultat d'une requête est-il disponible ?

  • 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 computed lu par le template, la question ne se pose pas : la vue existe déjà.
  • Comme output() et input(), 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.
  1. 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 crochet ngAfterViewInit.
    • @ViewChildren() renvoie une QueryList, qui prévient de ses changements par un Observable (changes). L'option static: true rend le résultat disponible plus tôt (dès ngOnInit) pour un élément qui n'est pas dans un @if ou un @for.
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

Correspondance entre @ViewChild() et viewChild()

  • Le ! de diaporama!: Diaporama promet à TypeScript que la propriété sera remplie, sans rien garantir. Lue trop tôt, par exemple dans ngOnInit, 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, et computed remplace ngAfterViewInit + changes.subscribe.
  • Angular fournit une migration automatique : ng generate @angular/core:signal-queries-migration.
  1. Aide-mémoire

Aide-mémoire de viewChild() dans Angular

  1. 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 #champ et identifiés par leur attribut name, et un bouton « S'inscrire ».
    • Un computed calcule 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 ! ».

Résultat attendu de l'activité 1 : le formulaire avec focus

  • Vérifier : formulaire vide → curseur dans « Nom » ; nom rempli → curseur dans « E-mail » ; e-mail correct et âge 12 → curseur dans « Âge ».
  • Pourquoi utiliser viewChildren avec une variable, plutôt que trois viewChild?
  1. 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 dans afterNextRender.
    • Dessiner avec les événements pointerdown, pointermove, pointerup et pointerleave (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.

Résultat attendu de l'activité 2 : le tableau de dessin

  • 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.
  1. Activité 3 : un quiz qui se corrige

    • Créer app-question: entrée obligatoire question (énoncé, choix, indice de la bonne réponse), entrée numero, signaux publics choisi et corrigee, computed public juste, méthodes corriger() et reinitialiser(). 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-quiz affiche trois questions et récupère les trois composants avec viewChildren(Question). Un computed affiche « 2 / 3 question(s) répondue(s) », puis « Score : 2 / 3 » après « Corriger » (qui appelle corriger() sur chaque question). « Recommencer » réinitialise tout.

Résultat attendu de l'activité 3 : le quiz corrigé

  • 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 ?
  1. Activité 4 : une boîte modale avec <dialog>

    • Créer app-modale autour de la balise native <dialog>: entrée titre, contenu projeté (<ng-content />), signal public ouverte, sortie fermee, et deux méthodes publiques ouvrir() (showModal()) et fermer() (close()), obtenues par viewChild sur la balise.
    • L'événement close du <dialog> remet ouverte à false et émet fermee: il se déclenche aussi avec la touche Échap.
    • Le parent app-page-inscription obtient la modale par viewChild.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 ».

Résultat attendu de l'activité 4 : la boîte modale ouverte

  • 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.
  1. Activité 5 : migrer @ViewChild() vers viewChild()

    • On vous fournit le composant suivant, qui affiche des étiquettes (app-etiquette, avec un signal public selectionnee) :
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 avec viewChild.required(), viewChildren(), afterNextRender() et des computed. 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.

Résultat attendu de l'activité 5 : avant et après la migration

  • Combien de lignes disparaissent ? Pourquoi n'y a-t-il plus d'abonnement à gérer ?
  • Lancer ng generate @angular/core:signal-queries-migration sur une copie de EtiquettesAncien et 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/).
  1. 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
  1. À retenir

    • viewChild(Composant) renvoie un signal contenant l'instance de l'enfant (ou undefined) ; 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'option read choisit 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 : afterNextRender pour le DOM, computed et effect pour réagir.
    • Une variable #ref suffit 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 par output().
    • @ViewChild() + ngAfterViewInit est l'ancienne écriture : à connaître, à migrer vers viewChild().
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.