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

Les variables de template dans Angular

Apprenez à faire réagir votre application Angular à chaque action de l’utilisateur. Vous écouterez les événements du navigateur, gérerez le clavier et ses raccourcis, maîtriserez la propagation et pre...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Apprenez à faire réagir votre application Angular à chaque action de l’utilisateur. Vous écouterez les événements du navigateur, gérerez le clavier et ses raccourcis, maîtriserez la propagation et preventDefault(), écouterez la fenêtre et le document, puis créerez vos propres événements avec output().

🎬 La leçon en vidéo : l'activité 2 corrigée pas à pas (9 min 52)

La vidéo ne s'affiche pas ? La regarder sur YouTube.

  1. Objectifs

    • Connaître les quatre sortes de variables de template: variables de référence #nom, déclarations @let, variables de bloc et variables d'entrée let-
    • Utiliser une variable de référence sur un élément HTML, un composant, une directive (exportAs) ou un <ng-template>
    • Afficher un modèle avec ngTemplateOutlet et lui transmettre un contexte
    • Atteindre une référence depuis la classe avec viewChild() et viewChildren(), au bon moment
    • Simplifier un template avec @let et les variables de @for et @if
    • Comprendre la portée des variables et éviter les pièges de nommage
  1. Présentation

    • Un template n'a accès qu'aux membres de sa classe. Il est pourtant souvent utile de nommer quelque chose directement dans le template : le champ dont on veut lire la valeur, le composant enfant à piloter, le résultat d'un calcul utilisé trois fois, la position d'un élément dans une boucle…
    • C'est le rôle des variables de template. Elles sont déclarées et utilisées dans le template, sans aucune ligne de TypeScript, et n'existent que dans celui-ci.
    • Vous en avez déjà rencontré : #zone dans « Le data binding dans Angular », #f="ngForm" dans « Le two-way data binding et ngModel », let i = $index dans « La syntaxe de base d'Angular ». Ce chapitre les rassemble et les approfondit.

Les quatre sortes de variables de template

  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/variables/. Tous les exemples ont été testés avec Angular 22.
  1. Une référence vers un élément HTML

    • #nom, placé sur une balise, crée une variable qui désigne l'élément du DOM lui-même : un HTMLInputElement pour un <input>, un HTMLDialogElement pour un <dialog>… On peut lire ses propriétés, appeler ses méthodes ou le transmettre à une méthode du composant.
    • La variable est utilisable partout dans le template, même avant la balise qui la déclare. L'écriture longue ref-nom existe aussi, mais elle est rarement utilisée.
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-reference-element',
  template: `
    <input #champ placeholder="Ville" (keyup.enter)="ajouter(champ)" />
    <button (click)="ajouter(champ)">Ajouter</button>
    <button (click)="champ.focus()">Placer le curseur</button>
    <p>Villes : {{ villes().join(', ') }} – le champ est un {{ champ.tagName }}</p>

    <input #curseurA type="range" value="40" />
    <p>Curseur A, sans écouteur : {{ curseurA.value }}</p>
    <input #curseurB type="range" value="40" (input)="0" />
    <p>Curseur B, avec (input) : {{ curseurB.value }}</p>

    <button (click)="boite.showModal()">Ouvrir la fenêtre</button>
    <dialog #boite>
      <p>Cette fenêtre est pilotée par la variable #boite.</p>
      <button (click)="boite.close()">Fermer</button>
    </dialog>
  `,
})
export class ReferenceElement {
  protected readonly villes = signal(['Tunis']);

  // La variable #champ est transmise à la méthode : c'est l'élément HTMLInputElement lui-même
  protected ajouter(champ: HTMLInputElement): void {
    if (!champ.value.trim()) return;
    this.villes.update((v) => [...v, champ.value.trim()]);
    champ.value = '';
    champ.focus();
  }
}
(Sousse + Entrée, puis Sfax + Ajouter)
Villes : Tunis, Sousse, Sfax – le champ est un INPUT        (le curseur revient dans le champ)
(curseur A déplacé à 75)   Curseur A, sans écouteur : 40      Curseur B, avec (input) : 40
(curseur B déplacé à 90)   Curseur A, sans écouteur : 75      Curseur B, avec (input) : 90
(Ouvrir la fenêtre)        la boîte <dialog> s'ouvre au premier plan ; Fermer la referme

Ce que désigne une variable de référence selon l'endroit où elle est posée

  • ajouter(champ) reçoit l'élément : la méthode lit sa valeur, le vide et lui redonne le focus. Le composant n'a besoin d'aucun signal pour le texte en cours de saisie.
  • boite.showModal() et boite.close() pilotent la balise native <dialog> sans une ligne de TypeScript.
  • Attention : une référence n'est pas un signal. Le curseur A ne prévient pas Angular : sa valeur affichée reste à 40 tant qu'aucun événement du template ne provoque de mise à jour. Le curseur B a un écouteur (input)="0" (une instruction qui ne fait rien) : chaque mouvement met le composant à jour… et le curseur A aussi, au passage.

Une variable de référence n'est relue qu'à la mise à jour du composant

  1. Une référence vers un composant enfant

    • Posée sur la balise d'un composant, la variable désigne son instance: le parent peut appeler ses méthodes et lire ses signals directement dans son template.
    • Seuls les membres publics de l'enfant sont accessibles. Les membres protected sont réservés à son propre template.
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-compteur-ref',
  template: `<span class="valeur">{{ valeur() }}</span>`,
  styles: `.valeur { display: inline-block; min-width: 40px; padding: 4px 10px; background: #e0f2fe; font: 700 18px monospace; text-align: center; }`,
})
export class CompteurRef {
  readonly valeur = signal(0); // public : visible depuis le template du parent
  protected readonly pas = 5; // protected : réservé au template de ce composant

  incrementer(): void {
    this.valeur.update((v) => v + 1);
  }

  reinitialiser(): void {
    this.valeur.set(0);
  }
}

@Component({
  selector: 'app-reference-composant',
  imports: [CompteurRef],
  template: `
    <app-compteur-ref #compteur />
    <button (click)="compteur.incrementer()">+1</button>
    <button (click)="compteur.reinitialiser()">Remise à zéro</button>
    <p>Valeur lue par le parent : {{ compteur.valeur() }} ({{ compteur.valeur() >= 3 ? 'objectif atteint' : 'en cours' }})</p>
  `,
})
export class ReferenceComposant {}
(3 × +1)          Valeur lue par le parent : 3 (objectif atteint)
(Remise à zéro)   Valeur lue par le parent : 0 (en cours)
  • Parce que valeur est un signal, l'affichage du parent suit chaque changement de l'enfant.
  • Attention : {{ compteur.pas }} est refusé : TS2445: Property 'pas' is protected and only accessible within class 'CompteurRef' and its subclasses.
  • Cette technique est pratique pour piloter un enfant depuis le template (un lecteur, un minuteur, un carrousel). Pour échanger des données, préférez input() et output() (voir « La gestion des événements dans Angular »).
  1. Une référence vers une directive : exportAs

    • Une directive peut se rendre accessible sous un nom choisi avec l'option exportAs. On l'atteint en donnant ce nom comme valeur à la variable : #c="compteur".
    • C'est ce que font ngModel et ngForm: #nom="ngModel" donne accès à l'état du champ (valid, touched, errors…), #f="ngForm" à celui du formulaire entier.
import { Directive, Component, computed, input, signal } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';

// Une directive qui compte les caractères d'un champ, exportée sous le nom « compteur »
@Directive({
  selector: '[appCompteur]',
  exportAs: 'compteur',
  host: { '(input)': 'longueur.set($any($event.target).value.length)' },
})
export class CompteurCaracteres {
  readonly max = input(100);
  readonly longueur = signal(0);
  readonly reste = computed(() => this.max() - this.longueur());
}

@Component({
  selector: 'app-export-as',
  imports: [FormsModule, JsonPipe, CompteurCaracteres],
  template: `
    <textarea appCompteur [max]="40" #c="compteur" #zone></textarea>
    <p [class.alerte]="c.reste() < 0">{{ c.longueur() }} / 40 – reste {{ c.reste() }} – élément : {{ zone.tagName }}</p>

    <form #f="ngForm">
      <input name="nom" ngModel required minlength="3" #nom="ngModel" placeholder="Nom" />
      @if (nom.invalid && nom.touched) {
        <span class="alerte">3 caractères minimum</span>
      }
      <p>Champ : {{ nom.value }} – valide : {{ nom.valid }} – formulaire : {{ f.valid ? 'valide' : 'invalide' }}</p>
      <p>Valeur du formulaire : {{ f.value | json }}</p>
    </form>
  `,
  styles: `.alerte { color: #dc2626; }`,
})
export class ExportAs {}
(« Les variables de template »)           25 / 40 – reste 15 – élément : TEXTAREA
(+ « dans Angular 22 !! »)                 44 / 40 – reste -4 – élément : TEXTAREA     (en rouge)
(« Am », puis sortie du champ)            3 caractères minimum
                                           Champ : Am – valide : false – formulaire : invalide
(« Amira »)                                Champ : Amira – valide : true – formulaire : valide
                                           Valeur du formulaire : { "nom": "Amira" }
  • Le même élément porte deux variables : #c="compteur" désigne la directive, #zone (sans valeur) désigne l'élément <textarea>.
  • Attention : sans valeur, la variable désigne toujours l'élément, même s'il porte une directive : #c puis c.longueur() donne TS2339: Property 'longueur' does not exist on type 'HTMLTextAreaElement'. Un nom d'export inconnu donne NG8003: No directive found with exportAs 'compteurX'.
  1. <ng-template>, let- et ngTemplateOutlet

    • Un <ng-template> déclare un modèle: son contenu n'est pas affiché. Une variable de référence #nom posée dessus désigne un TemplateRef, que la directive ngTemplateOutlet affiche où l'on veut, autant de fois qu'on le veut.
    • Le modèle reçoit des données par un contexte. Les variables d'entrée let- les récupèrent : let-s reçoit la valeur $implicit, let-rang="rang" reçoit la propriété rang du contexte.
import { Component, signal } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

interface Stagiaire { nom: string; ville: string; moyenne: number; }

@Component({
  selector: 'app-modeles',
  imports: [NgTemplateOutlet],
  template: `
    <!-- Deux modèles : rien n'est affiché tant qu'on ne les utilise pas -->
    <ng-template #compacte let-s let-rang="rang">
      <li>{{ rang }}. {{ s.nom }}</li>
    </ng-template>
    <ng-template #detaillee let-s let-rang="rang">
      <li>{{ rang }}. <strong>{{ s.nom }}</strong> ({{ s.ville }}) – {{ s.moyenne }} / 20</li>
    </ng-template>
    <ng-template #vide><p>Aucun stagiaire.</p></ng-template>

    <label><input type="checkbox" #detail (change)="0" /> Vue détaillée</label>
    <button (click)="stagiaires.set([])">Vider</button>
    <ul>
      @for (s of stagiaires(); track s.nom) {
        <ng-container
          [ngTemplateOutlet]="detail.checked ? detaillee : compacte"
          [ngTemplateOutletContext]="{ $implicit: s, rang: $index + 1 }"
        />
      } @empty {
        <ng-container [ngTemplateOutlet]="vide" />
      }
    </ul>
  `,
})
export class Modeles {
  protected readonly stagiaires = signal<Stagiaire[]>([
    { nom: 'Amira Ben Ali', ville: 'Sousse', moyenne: 16.25 },
    { nom: 'Ines Gharbi', ville: 'Sfax', moyenne: 13.75 },
  ]);
}
(vue compacte)      1. Amira Ben Ali | 2. Ines Gharbi
(Vue détaillée)     1. Amira Ben Ali (Sousse) – 16.25 / 20 | 2. Ines Gharbi (Sfax) – 13.75 / 20
(Vider)             Aucun stagiaire.

ng-template, let- et le contexte de ngTemplateOutlet

  • detail.checked ? detaillee : compacte choisit le modèle selon l'état de la case : deux variables de référence vers des ng-template et une vers une case à cocher, sans aucun code TypeScript.
  • NgTemplateOutlet s'importe depuis @angular/common. Le contexte est un objet ordinaire : { $implicit: s, rang: $index + 1 }.
  • Les variables let- d'un ng-template sont de type any: une faute dans s.nmo ne sera pas signalée. Gardez ces modèles courts.
  • Ce mécanisme permet surtout de créer des composants personnalisables, dont le parent fournit l'apparence (voir l'activité 5).
  1. Atteindre une référence depuis la classe : viewChild()

    • Les variables de template n'existent pas dans la classe. Pour agir depuis le code TypeScript, on utilise une requête de vue: viewChild('nom') retrouve l'élément marqué #nom, viewChild(Type) le premier composant ou la première directive de ce type, viewChildren() tous les éléments correspondants.
    • Ces fonctions renvoient des signals, mis à jour quand le template change. La version viewChild.required garantit une valeur (pas de undefined).
import { Component, ElementRef, Injector, afterNextRender, inject, signal, viewChild, viewChildren } from '@angular/core';
import { CompteurRef } from './reference-composant';

@Component({
  selector: 'app-requete',
  imports: [CompteurRef],
  template: `
    <input #recherche placeholder="Rechercher…" />
    <app-compteur-ref />
    <button (click)="ajouter()">Ajouter un message</button>
    <ul class="fil">
      @for (m of messages(); track $index) {
        <li #ligne>{{ m }}</li>
      }
    </ul>
    <p>{{ info() }}</p>
  `,
  styles: `.fil { height: 70px; overflow: auto; border: 1px solid #ccc; margin: 6px 0; }`,
})
export class Requete {
  // Par nom de variable : l'élément <input #recherche>
  private readonly recherche = viewChild.required<ElementRef<HTMLInputElement>>('recherche');
  // Par type : le premier composant CompteurRef du template (pas besoin de #)
  private readonly compteur = viewChild.required(CompteurRef);
  // Toutes les lignes #ligne, mises à jour quand la liste change
  private readonly lignes = viewChildren<ElementRef<HTMLLIElement>>('ligne');

  protected readonly messages = signal(['Bienvenue', 'Cours à 9 h', 'Pause à 10 h 30']);
  protected readonly info = signal('');
  private readonly injecteur = inject(Injector); // afterNextRender hors du constructeur en a besoin

  constructor() {
    // Le template n'existe pas encore dans le constructeur : on attend le premier rendu
    afterNextRender(() => this.recherche().nativeElement.focus());
  }

  protected ajouter(): void {
    this.compteur().incrementer();
    this.messages.update((m) => [...m, `Message n°${this.compteur().valeur()}`]);
    // Après le rendu, la nouvelle ligne existe : on la fait défiler jusqu'à elle
    afterNextRender(() => {
      this.lignes().at(-1)?.nativeElement.scrollIntoView({ block: 'nearest' });
      this.info.set(`${this.lignes().length} lignes – la dernière est visible`);
    }, { injector: this.injecteur });
  }
}
(au chargement)     le curseur est dans le champ « Rechercher… »
(2 × Ajouter)       compteur : 2 – lignes : Bienvenue, Cours à 9 h, Pause à 10 h 30, Message n°1, Message n°2
                    5 lignes – la dernière est visible           (la liste a défilé jusqu'en bas)

viewChild et viewChildren : relier le template et la classe

  • La requête par type (viewChild.required(CompteurRef)) n'a pas besoin de variable #: on obtient directement l'instance du composant et ses méthodes.
  • Le template n'existe pas encore dans le constructeur. afterNextRender() exécute le code après le prochain rendu : c'est le bon moment pour donner le focus ou faire défiler la page. Hors du constructeur, il faut lui passer l'injecteur.
  • Attention : lire une requête required dans le constructeur est détecté dès la compilation : NG8118: `champ` is a required `viewChild` and does not have a value in this context.
  1. Les déclarations locales @let

    • @let nom = expression; déclare une variable locale dans le template. Elle évite de répéter un calcul, rend le template plus lisible et fonctionne avec les pipes (| async, | number…).
    • Sa valeur est recalculée à chaque mise à jour du composant, comme une interpolation. Elle est en lecture seule et n'est visible que dans le bloc où elle est déclarée.
import { Component, signal } from '@angular/core';
import { AsyncPipe, DecimalPipe } from '@angular/common';
import { interval, map } from 'rxjs';

@Component({
  selector: 'app-let-local',
  imports: [AsyncPipe, DecimalPipe],
  template: `
    @let total = prix() * quantite();
    @let remise = total >= 500 ? total * 0.1 : 0;
    @let aPayer = total - remise;

    <button (click)="quantite.set(quantite() + 1)">+1</button>
    <p>{{ quantite() }} × {{ prix() }} DT = {{ total }} DT – remise : {{ remise }} DT – à payer : {{ aPayer }} DT</p>

    @let secondes = chrono$ | async;
    <p>Ouvert depuis {{ secondes ?? 0 }} s</p>

    @let moyenneGenerale = 13.5;
    <ul>
      @for (s of stagiaires; track s.nom) {
        @let ecart = s.moyenne - moyenneGenerale;
        <li [class.plus]="ecart >= 0">{{ s.nom }} : {{ ecart >= 0 ? '+' : '' }}{{ ecart | number: '1.2-2' }}</li>
      }
    </ul>
  `,
  styles: `li { color: #dc2626; } li.plus { color: #15803d; }`,
})
export class LetLocal {
  protected readonly prix = signal(120);
  protected readonly quantite = signal(3);
  protected readonly chrono$ = interval(1000).pipe(map((n) => n + 1));
  protected readonly stagiaires = [
    { nom: 'Amira', moyenne: 16.25 },
    { nom: 'Sami', moyenne: 9.5 },
    { nom: 'Salma', moyenne: 14.5 },
  ];
}
3 × 120 DT = 360 DT – remise : 0 DT – à payer : 360 DT
Ouvert depuis 5 s
Amira : +2,75 | Sami : -4,00 | Salma : +1,00
(2 × +1)    5 × 120 DT = 600 DT – remise : 60 DT – à payer : 540 DT

Les règles des déclarations @let

  • @let ecart, déclarée dans le @for, est recalculée pour chaque stagiaire et n'existe qu'à l'intérieur de la boucle.
  • @let secondes = chrono$ | async; remplace l'ancienne astuce *ngIf="chrono$ | async as secondes".
  • Erreurs à connaître : (click)="total = 0" → NG8015: Cannot assign to @let declaration 'total'.; utiliser avant la déclaration → NG8016: Cannot read @let declaration 'total' before it has been defined.; deux symboles de même nom → NG8017: Cannot declare @let called 'total' as there is another symbol in the template with the same name.
  • Une @let sert à l'affichage. Si la valeur est aussi utile dans la classe, déclarez plutôt un computed().
  1. Les variables de bloc : @for et @if

    • @for fournit des variables implicites : $index, $count, $first, $last, $even et $odd. On leur donne un nom plus parlant avec let: let i = $index, premier = $first.
    • @if (expression; as nom) enregistre le résultat de la condition dans une variable, utilisable dans le bloc.
import { Component, computed, signal } from '@angular/core';

interface Stagiaire { nom: string; moyenne: number; }

@Component({
  selector: 'app-blocs',
  template: `
    <ol>
      @for (s of tries(); track s.nom; let i = $index, premier = $first, dernier = $last, pair = $even, n = $count) {
        <li [class.pair]="pair">
          {{ i + 1 }}/{{ n }} – {{ s.nom }} ({{ s.moyenne }})
          @if (premier) { <strong>major</strong> }
          @if (dernier) { <em>dernier</em> }
        </li>
      } @empty {
        <li>Aucun stagiaire</li>
      }
    </ol>

    @if (meilleur(); as m) {
      <p>Meilleure moyenne : {{ m.nom }} avec {{ m.moyenne }}</p>
    } @else {
      <p>Pas encore de classement</p>
    }
    <button (click)="stagiaires.set([])">Vider</button>
  `,
  styles: `.pair { background: #f1f5f9; }`,
})
export class Blocs {
  protected readonly stagiaires = signal<Stagiaire[]>([
    { nom: 'Ines', moyenne: 13.75 }, { nom: 'Amira', moyenne: 16.25 }, { nom: 'Sami', moyenne: 9.5 }, { nom: 'Youssef', moyenne: 11 },
  ]);
  protected readonly tries = computed(() => [...this.stagiaires()].sort((a, b) => b.moyenne - a.moyenne));
  protected readonly meilleur = computed(() => this.tries()[0]);
}
1/4 – Amira (16.25) major
2/4 – Ines (13.75)
3/4 – Youssef (11)
4/4 – Sami (9.5) dernier
Meilleure moyenne : Amira avec 16.25
(Vider)     Aucun stagiaire – Pas encore de classement
  • Les alias sont utiles dans les boucles imbriquées : dans un @for placé dans un autre, $index désigne toujours la boucle la plus proche ; un alias (let ligne = $index) permet de garder l'index de la boucle extérieure.
  • @if (meilleur(); as m) lit le signal une seule fois, et m est du bon type (sans undefined) dans le bloc.
  1. La portée des variables et les pièges de nommage

    • Une variable déclarée dans un bloc (@for, @if, @switch…) n'est visible que dans ce bloc et dans les blocs qu'il contient. {{ i }} après le @for donne TS2339: Property 'i' does not exist on type …, et une référence #champ déclarée dans un @if n'est pas utilisable après ce @if.
    • Une variable de template masque la propriété du composant qui porte le même nom, sans aucun message : avec <input #nom />, {{ nom }} affiche [object HTMLInputElement] au lieu de la propriété nom. {{ this.nom }} lit la propriété.
    • Deux références identiques (#a sur deux éléments) ne provoquent pas d'erreur : seule la première est utilisée. Choisissez des noms uniques et explicites.
    • Une référence est une constante : (click)="champ = null" est refusé (TS2322: Type 'null' is not assignable to type 'HTMLInputElement'.).

La portée des variables de template

  1. Aide-mémoire

Aide-mémoire des variables de template Angular

  1. Activité 1 : des fenêtres de confirmation avec <dialog>

    • Générer le composant act-variables/confirmation qui affiche une liste d'inscriptions ({ id, nom, formation }). Chaque ligne a deux boutons :
      • Détails ouvre une fenêtre <dialog #fiche> avec fiche.showModal(); son bouton Fermer appelle fiche.close();
      • Supprimer ouvre <dialog #confirmation>, qui contient un <form method="dialog"> avec deux boutons value="oui" et value="non";
      • à l'événement (close), lire confirmation.returnValue et le transmettre à une méthode decider(): supprimer si « oui », sinon afficher « Suppression annulée (non) » ou « (touche Échap) ».
    • Un signal choisie retient l'inscription concernée. Aucune autre variable n'est nécessaire dans la classe : les fenêtres sont pilotées par leurs références.

Résultat attendu de l'activité 1 : la fenêtre de confirmation ouverte

  • Ouvrir la confirmation puis appuyer sur Échap : que vaut returnValue? Pourquoi faut-il le remettre à '' après chaque fermeture ?
  • Qu'apporte showModal() par rapport à show() (fond assombri, touche Échap, focus) ?
  1. Activité 2 : un formulaire d'inscription piloté par ses références

    • Générer le composant act-variables/inscription avec un formulaire #f="ngForm" de quatre champs : nom (3 caractères minimum), e-mail, mot de passe (8 caractères minimum) et confirmation. Chaque champ a sa variable (#nom="ngModel", #email="ngModel"…) :
      • les messages d'erreur s'affichent quand le champ est invalide et a été visité (nom.invalid && nom.touched) ;
      • une jauge de force du mot de passe utilise mdp.value (rouge, orange ou vert) ; la confirmation compare conf.value et mdp.value;
      • le bouton S'inscrire est désactivé tant que f.invalid ou que les mots de passe diffèrent ; un texte indique le nombre de champs à corriger ;
      • (ngSubmit)="envoyer(f)" transmet le formulaire à la méthode, qui affiche un message de bienvenue puis appelle f.resetForm().

Résultat attendu de l'activité 2 : formulaire prêt à envoyer

  • Saisir « Am », « amira@ », « angular » et « angula » : vérifier les trois messages et « 3 champ(s) à corriger ». Pourquoi le mot de passe « angular » est-il invalide ?
  • Pourquoi le bouton reste-t-il désactivé quand tous les champs sont valides mais que la confirmation est différente ?

🎬 Correction en vidéo : cette activité est corrigée pas à pas, du projet de départ au test final, dans la vidéo de la leçon (à partir de 2:24).

  1. Activité 3 : un minuteur piloté par son parent

    • Créer un composant enfant app-minuteur: une entrée duree (en secondes), des signals publics restant et enMarche, un computed affichage au format mm:ss, et deux méthodes publiques basculer() et reinitialiser(). Un effet fait tourner le minuteur (setInterval et onCleanup).
    • Le parent act-variables/tableau-minuteur:
      • propose trois durées (1, 3 et 5 min) ;
      • place <app-minuteur #m [duree]="duree()" /> et pilote l'enfant depuis son template: m.basculer(), m.reinitialiser(), une barre de progression calculée avec m.restant(), un bouton Démarrer / Pause / Reprendre et un texte d'état ;
      • pilote le même enfant depuis sa classe pour le raccourci Espace : viewChild.required(Minuteur) et un écouteur '(document:keydown.space)' dans host.

Résultat attendu de l'activité 3 : minuteur de 3 minutes en marche

  • Pourquoi restant et basculer() doivent-ils être publics, alors que les membres du parent sont protected?
  • Supprimer e.preventDefault() dans le raccourci Espace et tester sur une page qui défile : que se passe-t-il ?
  1. Activité 4 : un classement avec @for et @let

    • Générer le composant act-variables/classement qui affiche les six stagiaires triés par moyenne décroissante, avec un filtre par ville (<select #filtre (change)="ville.set(filtre.value)">). Dans le template :
      • @let moyenneGenerale = moyenne(); avant le tableau ;
      • dans le @for, les alias rang, premier, dernier, pair et total: 🥇 pour le premier, lignes paires grisées, trait rouge sous le dernier (seulement s'il y a plus d'une ligne) ;
      • deux @let par ligne : ecart (vert si positif, rouge sinon) et mention (Très bien, Bien, Assez bien, Passable, Insuffisant) ;
      • un bloc @empty et un pied de tableau « N stagiaire(s) – moyenne : … ».

Résultat attendu de l'activité 4 : le classement des six stagiaires

  • Filtrer sur Tunis : vérifier « Karim Mansour +1,38 » et « moyenne : 10,88 ». Pourquoi les écarts ont-ils changé ?
  • Pourquoi @let moyenneGenerale est-elle déclarée hors du @for, et @let ecart à l'intérieur ?
  1. Activité 5 : une liste dont le parent choisit l'apparence

    • Créer un composant générique app-liste-modele<T> avec trois entrées : titre, elements (T[]) et modele, de type TemplateRef<ContexteLigne<T>> (facultatif). Pour chaque élément, il affiche le modèle reçu avec le contexte { $implicit: e, index: $index }, ou un modèle par défaut <ng-template #parDefaut let-e>.
    • Le parent act-variables/catalogue:
      • déclare deux modèles, #ligneSimple (« 1. Angular ») et #ligneDetaillee (titre en gras, badge de niveau, nombre d'heures), avec let-f et let-i="index";
      • affiche les formations avec [modele]="cartes.checked ? ligneDetaillee : ligneSimple", où #cartes est une case à cocher ;
      • réutilise le même composant pour une liste de villes, sans modèle (affichage par défaut).

Résultat attendu de l'activité 5 en affichage détaillé

  • Pourquoi la case à cocher a-t-elle besoin de (change)="0"? Remplacer cette astuce par un signal.
  • Ajouter un troisième modèle (par exemple un tableau) sans modifier app-liste-modele. Quel principe de conception cela illustre-t-il ?
  • Faire un commit après chaque activité, puis un push : git commit -m "Variables de template : activité N".
  1. Erreurs fréquentes

Message ou symptôme Cause Solution
TS2339: Property 'champ' does not exist on type 'Erreur'. Variable utilisée hors du bloc qui la déclare (@if, @for), ou faute de frappe Utiliser la variable dans son bloc, ou la déclarer plus haut
TS2445: Property 'pas' is protected and only accessible within class 'CompteurRef' … Membre protected ou private d'un enfant lu par le parent Rendre le membre public, ou passer par un output()
TS2339: Property 'longueur' does not exist on type 'HTMLTextAreaElement'. #c sans valeur : la variable désigne l'élément #c="compteur" (nom donné par exportAs)
NG8003: No directive found with exportAs 'compteurX'. Nom d'export inconnu, ou directive non importée Vérifier exportAs et le tableau imports (FormsModule pour ngModel)
NG8015: Cannot assign to @let declaration 'total'. Affectation d'une @let Modifier le signal d'origine
NG8016: Cannot read @let declaration 'total' before it has been defined. @let utilisée avant sa ligne Déplacer la déclaration plus haut
NG8017: Cannot declare @let called 'total' as there is another symbol … Deux @let, ou une @let et un #total, de même nom Choisir des noms différents
NG8118: `champ` is a required `viewChild` and does not have a value in this context. Requête lue dans le constructeur La lire dans afterNextRender() ou une méthode
NG8112: @let total is declared but its value is never read. (avertissement) @let inutilisée La supprimer
TS2322: Type 'null' is not assignable to type 'HTMLInputElement'. Affectation d'une variable de référence Modifier l'élément (champ.value = ''), pas la variable
L'affichage ne suit pas {{ curseur.value }} Aucun événement du template ne met le composant à jour Ajouter un écouteur, ou utiliser un signal / ngModel
{{ nom }} affiche [object HTMLInputElement] #nom masque la propriété nom Renommer la variable, ou écrire this.nom
Un modèle ng-template n'apparaît pas Aucun ngTemplateOutlet, ou NgTemplateOutlet non importé [ngTemplateOutlet]="modele" et l'import depuis @angular/common
  1. À retenir

    • #nom désigne un élément HTML, l'instance d'un composant, une directive (#n="ngModel", grâce à exportAs) ou un TemplateRef.
    • Une référence n'est pas réactive : elle est relue à chaque mise à jour du composant, déclenchée par un événement du template ou un signal.
    • <ng-template> + ngTemplateOutlet + let- permettent de réutiliser et de personnaliser des morceaux de template.
    • viewChild() et viewChildren() relient les références à la classe ; on les lit après le rendu (afterNextRender).
    • @let nomme le résultat d'une expression ; @for fournit $index, $first, $last, $even, $count; @if (…; as x) nomme la condition.
    • Une variable n'est visible que dans son bloc ; elle masque une propriété de même nom.
Nommez dans le template ce qui ne sert qu'au template, et gardez dans la classe ce qui porte la logique : vos composants resteront courts, lisibles et faciles à faire évoluer.