À 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.
-
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éelet- - 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
ngTemplateOutletet lui transmettre un contexte - Atteindre une référence depuis la classe avec
viewChild()etviewChildren(), au bon moment - Simplifier un template avec
@letet les variables de@foret@if - Comprendre la portée des variables et éviter les pièges de nommage
- Connaître les quatre sortes de variables de template: variables de référence
-
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é :
#zonedans « Le data binding dans Angular »,#f="ngForm"dans « Le two-way data binding et ngModel »,let i = $indexdans « La syntaxe de base d'Angular ». Ce chapitre les rassemble et les approfondit.

- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/variables/. Tous les exemples ont été testés avec Angular 22.
-
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 : unHTMLInputElementpour un<input>, unHTMLDialogElementpour 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-nomexiste 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

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()etboite.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 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
protectedsont 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
valeurest 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()etoutput()(voir « La gestion des événements dans Angular »).
-
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
ngModeletngForm:#nom="ngModel"donne accès à l'état du champ (valid,touched,errors…),#f="ngForm"à celui du formulaire entier.
- Une directive peut se rendre accessible sous un nom choisi avec l'option
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 :
#cpuisc.longueur()donneTS2339: Property 'longueur' does not exist on type 'HTMLTextAreaElement'.Un nom d'export inconnu donneNG8003: No directive found with exportAs 'compteurX'.
-
<ng-template>,let-etngTemplateOutlet- Un
<ng-template>déclare un modèle: son contenu n'est pas affiché. Une variable de référence#nomposée dessus désigne unTemplateRef, que la directivengTemplateOutletaffiche 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-sreçoit la valeur$implicit,let-rang="rang"reçoit la propriétérangdu contexte.
- Un
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.

detail.checked ? detaillee : compactechoisit le modèle selon l'état de la case : deux variables de référence vers desng-templateet une vers une case à cocher, sans aucun code TypeScript.NgTemplateOutlets'importe depuis@angular/common. Le contexte est un objet ordinaire :{ $implicit: s, rang: $index + 1 }.- Les variables
let-d'unng-templatesont de typeany: une faute danss.nmone 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).
-
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.requiredgarantit une valeur (pas deundefined).
- Les variables de template n'existent pas dans la classe. Pour agir depuis le code TypeScript, on utilise une requête de vue:
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)

- 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
requireddans le constructeur est détecté dès la compilation :NG8118: `champ` is a required `viewChild` and does not have a value in this context.
-
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

@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
@letsert à l'affichage. Si la valeur est aussi utile dans la classe, déclarez plutôt uncomputed().
-
Les variables de bloc :
@foret@if@forfournit des variables implicites :$index,$count,$first,$last,$evenet$odd. On leur donne un nom plus parlant aveclet: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
@forplacé dans un autre,$indexdé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, etmest du bon type (sansundefined) dans le bloc.
-
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@fordonneTS2339: Property 'i' does not exist on type …, et une référence#champdéclarée dans un@ifn'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 (
#asur 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'.).
- Une variable déclarée dans un bloc (

-
Aide-mémoire

-
Activité 1 : des fenêtres de confirmation avec
<dialog>- Générer le composant
act-variables/confirmationqui affiche une liste d'inscriptions ({ id, nom, formation }). Chaque ligne a deux boutons :- Détails ouvre une fenêtre
<dialog #fiche>avecfiche.showModal(); son bouton Fermer appellefiche.close(); - Supprimer ouvre
<dialog #confirmation>, qui contient un<form method="dialog">avec deux boutonsvalue="oui"etvalue="non"; - à l'événement
(close), lireconfirmation.returnValueet le transmettre à une méthodedecider(): supprimer si « oui », sinon afficher « Suppression annulée (non) » ou « (touche Échap) ».
- Détails ouvre une fenêtre
- Un signal
choisieretient 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.
- Générer le composant

- 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) ?
-
Activité 2 : un formulaire d'inscription piloté par ses références
- Générer le composant
act-variables/inscriptionavec 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 compareconf.valueetmdp.value; - le bouton S'inscrire est désactivé tant que
f.invalidou 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 appellef.resetForm().
- les messages d'erreur s'affichent quand le champ est invalide et a été visité (
- Générer le composant

- 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).
-
Activité 3 : un minuteur piloté par son parent
- Créer un composant enfant
app-minuteur: une entréeduree(en secondes), des signals publicsrestantetenMarche, uncomputedaffichageau formatmm:ss, et deux méthodes publiquesbasculer()etreinitialiser(). Un effet fait tourner le minuteur (setIntervaletonCleanup). - 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 avecm.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)'danshost.
- Créer un composant enfant

- Pourquoi
restantetbasculer()doivent-ils être publics, alors que les membres du parent sontprotected? - Supprimer
e.preventDefault()dans le raccourci Espace et tester sur une page qui défile : que se passe-t-il ?
-
Activité 4 : un classement avec
@foret@let- Générer le composant
act-variables/classementqui 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 aliasrang,premier,dernier,pairettotal: 🥇 pour le premier, lignes paires grisées, trait rouge sous le dernier (seulement s'il y a plus d'une ligne) ; - deux
@letpar ligne :ecart(vert si positif, rouge sinon) etmention(Très bien, Bien, Assez bien, Passable, Insuffisant) ; - un bloc
@emptyet un pied de tableau « N stagiaire(s) – moyenne : … ».
- Générer le composant

- Filtrer sur Tunis : vérifier « Karim Mansour +1,38 » et « moyenne : 10,88 ». Pourquoi les écarts ont-ils changé ?
- Pourquoi
@let moyenneGeneraleest-elle déclarée hors du@for, et@let ecartà l'intérieur ?
-
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[]) etmodele, de typeTemplateRef<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), aveclet-fetlet-i="index"; - affiche les formations avec
[modele]="cartes.checked ? ligneDetaillee : ligneSimple", où#cartesest une case à cocher ; - réutilise le même composant pour une liste de villes, sans modèle (affichage par défaut).
- déclare deux modèles,
- Créer un composant générique

- 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".
-
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 |
-
À retenir
#nomdésigne un élément HTML, l'instance d'un composant, une directive (#n="ngModel", grâce àexportAs) ou unTemplateRef.- 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()etviewChildren()relient les références à la classe ; on les lit après le rendu (afterNextRender).@letnomme le résultat d'une expression ;@forfournit$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.