À propos de ce cours
Un composant Angular associe une classe TypeScript, un template HTML et des styles, reliés par une syntaxe propre à Angular. Ce chapitre présente l’anatomie d’un composant, puis les quatre liaisons de données : l’interpolation {{ }} pour afficher, la liaison de propriété [ ] pour régler un attribut, une classe ou un style, la liaison d’événement ( ) pour réagir aux actions, et [(ngModel)] pour synchroniser un formulaire. Vous découvrirez aussi les variables de référence et les blocs de contrôle @if, @for, @switch et @let. Enfin, vous formaterez l’affichage avec les pipes, en français. Cinq activités guidées (carte de profil, compteur, liste de tâches, convertisseur, statut de sessions) mettent toute cette syntaxe en pratique.
-
Objectifs
- Connaître l'anatomie d'un composant : classe, template, styles
- Afficher des données avec l'interpolation
{{ }} - Lier une propriété HTML avec
[ ], une classe ou un style dynamiques - Réagir aux actions de l'utilisateur avec la liaison d'événement
( ) - Utiliser les variables de référence
#nomet la liaison bidirectionnelle[(ngModel)] - Afficher selon une condition ou répéter avec
@if,@for,@switchet@let - Formater l'affichage avec les pipes, en français
-
Présentation
- Une application Angular est un assemblage de composants. Chaque composant associe :
- une classe TypeScript, qui contient les données et les méthodes ;
- un template HTML, qui décrit l'affichage ;
- des styles CSS, limités à ce composant.
- Le template est du HTML enrichi d'une syntaxe propre à Angular. Elle sert à faire circuler les informations entre la classe et la page : c'est la liaison de données (data binding). Quand une donnée change, Angular met la page à jour automatiquement.
- Il existe quatre formes de liaison, à reconnaître à leurs symboles :
- Une application Angular est un assemblage de composants. Chaque composant associe :

- Remarque : les exemples de ce chapitre ont été testés avec Angular 22. Ils utilisent les signals (
signal(),computed()) pour les données qui changent : c'est la manière actuelle d'écrire un composant, et elle permet à Angular de savoir exactement ce qu'il doit mettre à jour.
L'anatomie d'un composant
- Créer le projet du chapitre, puis le lancer :
npx @angular/cli@latest new premiers-pas-angular --defaults --ssr=false --style=css
cd premiers-pas-angular
npx ng serve # puis ouvrir http://localhost:4200
- Le composant racine se trouve dans
src/app/. Voiciapp.ts, tel que le génère Angular :
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
imports: [RouterOutlet],
selector: 'app-root',
styleUrl: './app.css',
templateUrl: './app.html',
})
export class App {
protected readonly title = signal('premiers-pas-angular');
}

@Componentest le décorateur vu au chapitre précédent : il transforme la classe en composant.selectorest le nom de la balise qui affiche le composant :<app-root></app-root>se trouve danssrc/index.html.importsliste ce que le template utilise : autres composants, directives et pipes. Ce qui n'y figure pas est inconnu du template.templateUrl/styleUrldésignent les fichiers du template et des styles. Pour un petit composant, on écrit directementtemplate: `…`etstyles: `…`: c'est ce que font les exemples de ce chapitre.- Les membres sont déclarés
protected: ils sont accessibles depuis le template, mais pas depuis le code des autres classes. - Pour créer un nouveau composant :
npx ng generate component activites/carte-profil(oung g c). Angular créecarte-profil.ts,carte-profil.html,carte-profil.csset la classeCarteProfil, de sélecteurapp-carte-profil.
L'interpolation {{ }}
- Les doubles accolades affichent la valeur d'une expression de la classe, convertie en texte. On peut y écrire une propriété, une opération, un ternaire, un appel de méthode ou les opérateurs
?.et??. - Un signal se lit en l'appelant:
{{ session() }}.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-interpolation',
template: `
<h2>{{ titre }}</h2>
<p>Formateur : {{ formateur.prenom }} {{ formateur.nom.toUpperCase() }}</p>
<p>Places restantes : {{ places - inscrits }} / {{ places }}</p>
<p>Statut : {{ inscrits >= places ? 'complet' : 'ouvert' }}</p>
<p>Prix TTC : {{ calculerTTC(480) }} DT</p>
<p>Session : {{ session() }}</p>
<p>Salle : {{ salle ?? 'à définir' }}</p>
`,
})
export class Interpolation {
protected titre = 'Formation Angular';
protected formateur = { prenom: 'Ali', nom: 'Ben Salah' };
protected places = 15;
protected inscrits = 12;
protected salle: string | null = null;
protected session = signal('Novembre 2026');
protected calculerTTC(ht: number): number {
return ht * 1.19;
}
}
Formation Angular
Formateur : Ali BEN SALAH
Places restantes : 3 / 15
Statut : ouvert
Prix TTC : 571.1999999999999 DT
Session : Novembre 2026
Salle : à définir
- Le prix affiché
571.1999999999999montre qu'il ne faut pas afficher un calcul brut : la section « Les pipes » donne la solution. - Ce qui est interdit dans une interpolation : les affectations (
=,+=,++),new, les instructions (if,for), les expressions qui ont des effets de bord. Une interpolation lit des données, elle ne les modifie pas. - Attention : oublier les parenthèses d'un signal (
{{ session }}) n'affiche pas sa valeur. Angular le signale par un avertissement :NG8109: session is a function and should be invoked: session().
La liaison de propriété [ ]
- Les crochets lient une propriété d'un élément HTML à une expression :
[src]="logo". La valeur entre guillemets est une expression TypeScript, pas un texte : pour un texte fixe, il faut des apostrophes ([alt]="'Logo'"). - Pour une chaîne, l'interpolation dans un attribut est équivalente :
src="{{ logo }}". Pour un booléen ou un nombre (disabled,value…), utilisez les crochets. - Pour un attribut HTML qui n'a pas de propriété correspondante (
colspan,aria-*,data-*), on écrit[attr.nom].
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-proprietes',
template: `
<img [src]="logo" [alt]="'Logo ' + nom" width="64" />
<img src="{{ logo }}" alt="Logo {{ nom }}" width="64" />
<button [disabled]="complet()">Réserver</button>
<input [value]="nom" [placeholder]="aide" />
<td [attr.colspan]="colonnes">Total</td>
<button [attr.aria-label]="'Supprimer ' + nom">✕</button>
`,
})
export class Proprietes {
protected logo = 'favicon.ico';
protected nom = 'Angular';
protected aide = 'Votre nom';
protected colonnes = 3;
protected complet = signal(false);
}
- Classes et styles dynamiques:
[class.nom]ajoute une classe CSS si l'expression est vraie ;[style.propriete]règle un style, avec une unité si besoin ([style.width.%],[style.font-size.px]) ;[class]accepte aussi un objet.
<p [class.alerte]="complet()" [style.color]="complet() ? 'crimson' : 'seagreen'">
{{ complet() ? 'Complet' : 'Places disponibles' }}
</p>
<div class="barre" [style.width.%]="taux"></div>
<p [class]="{ gras: true, barre: complet() }">Classes multiples</p>
-
La liaison d'événement
( )- Les parenthèses écoutent un événement du DOM (
click,input,change,submit,keyup…) et exécutent une instruction, le plus souvent un appel de méthode. $eventcontient l'objet événement. Sur un<input>, Angular sait que$event.targetest unHTMLInputElement: on peut lire directement$event.target.value.- Angular propose des filtres de touches:
(keyup.enter),(keyup.escape),(keydown.control.s)…
- Les parenthèses écoutent un événement du DOM (
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-evenements',
template: `
<button (click)="ajouter()">+1</button>
<button (click)="compteur.set(0)">Remettre à zéro</button>
<p>Compteur : {{ compteur() }}</p>
<input (input)="prenom.set($event.target.value)" placeholder="Votre prénom" />
<p>Bonjour {{ prenom() || 'inconnu' }}</p>
<input #champ (keyup.enter)="valider(champ.value); champ.value = ''" placeholder="Ville puis Entrée" />
<p>Dernière ville : {{ ville() }}</p>
`,
})
export class Evenements {
protected compteur = signal(0);
protected prenom = signal('');
protected ville = signal('—');
protected ajouter(): void {
this.compteur.update((n) => n + 1);
}
protected valider(valeur: string): void {
this.ville.set(valeur.trim());
}
}
- Après trois clics sur
+1, la saisie de « Sami » et de « Sousse » suivie d'Entrée, la page afficheCompteur : 3,Bonjour SamietDernière ville : Sousse, et le champ de la ville est vidé. - Contrairement à l'interpolation, une liaison d'événement peut modifier des données, et enchaîner plusieurs instructions séparées par
;. - Pour modifier un signal, on appelle
set()ouupdate(): écrirecompteur = 5est refusé (TS2322: Type 'number' is not assignable to type 'WritableSignal<number>').
Les variables de référence #
#nomplacé sur un élément crée une variable utilisable partout dans le template, qui désigne cet élément. Dans l'exemple précédent,#champdonne accès au champ de saisie :champ.valuelit son contenu,champ.value = ''le vide.- C'est la manière la plus simple de lire un champ au moment d'un événement, sans stocker chaque frappe dans la classe.
<input #recherche placeholder="Rechercher" />
<button (click)="chercher(recherche.value)">OK</button>
<button (click)="recherche.focus()">Placer le curseur</button>
-
La liaison bidirectionnelle
[( )]- La liaison bidirectionnelle combine une liaison de propriété et une liaison d'événement : la donnée s'affiche dans le champ et chaque modification du champ met la donnée à jour. Sa syntaxe
[( )]est surnommée la « banane dans une boîte ». - Pour les champs de formulaire, on utilise la directive
ngModel, qui impose d'importerFormsModuledans le composant. Elle fonctionne directement avec un signal.
- La liaison bidirectionnelle combine une liaison de propriété et une liaison d'événement : la donnée s'affiche dans le champ et chaque modification du champ met la donnée à jour. Sa syntaxe
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-bidirectionnelle',
imports: [FormsModule],
template: `
<input [(ngModel)]="nom" placeholder="Nom" />
<input type="number" [(ngModel)]="quantite" min="1" />
<label><input type="checkbox" [(ngModel)]="livraison" /> Livraison</label>
<p>{{ nom() }} commande {{ quantite() }} article(s) {{ livraison() ? 'avec' : 'sans' }} livraison.</p>
`,
})
export class Bidirectionnelle {
protected nom = signal('Amira');
protected quantite = signal(1);
protected livraison = signal(false);
}
Amira commande 1 article(s) sans livraison.
(après saisie de Karim, 3 et cochage)
Karim commande 3 article(s) avec livraison.
- Attention : sans
FormsModuledansimports, la compilation échoue :NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'.
Le contrôle de flux : @if, @for, @switch
- Les blocs qui commencent par
@affichent une partie du template selon une condition, ou la répètent pour chaque élément d'une liste. @ifaccepte des@else ifet un@else. Avecas, on peut nommer la valeur testée.@forrépète un bloc ;trackest obligatoire: il indique à Angular comment reconnaître chaque élément (en général sonid), pour ne mettre à jour que ce qui a changé.@emptys'affiche si la liste est vide.- Dans
@for, les variables$index,$first,$last,$even,$oddet$countsont disponibles, et renommables aveclet. @switchchoisit un bloc selon une valeur, avec des@caseet un@default.

import { Component, computed, signal } from '@angular/core';
interface Formation {
id: number;
titre: string;
niveau: 'debutant' | 'intermediaire' | 'avance';
places: number;
}
@Component({
selector: 'app-controle',
template: `
@if (formations().length === 0) {
<p>Aucune formation pour le moment.</p>
} @else if (formations().length === 1) {
<p>Une seule formation disponible.</p>
} @else {
<p>{{ formations().length }} formations disponibles.</p>
}
<ul>
@for (f of formations(); track f.id; let i = $index, premier = $first, dernier = $last) {
<li [class.premier]="premier">
{{ i + 1 }}. {{ f.titre }}
@switch (f.niveau) {
@case ('debutant') { <span>🟢 débutant</span> }
@case ('intermediaire') { <span>🟠 intermédiaire</span> }
@default { <span>🔴 avancé</span> }
}
@if (dernier) { <em>(dernière)</em> }
</li>
} @empty {
<li>La liste est vide.</li>
}
</ul>
@let total = totalPlaces();
<p>Total : {{ total }} place{{ total > 1 ? 's' : '' }}</p>
@if (selection(); as f) {
<p>Sélection : {{ f.titre }}</p>
}
`,
})
export class Controle {
protected formations = signal<Formation[]>([
{ id: 1, titre: 'HTML et CSS', niveau: 'debutant', places: 20 },
{ id: 2, titre: 'Angular', niveau: 'intermediaire', places: 15 },
{ id: 3, titre: 'Flutter', niveau: 'avance', places: 10 },
]);
protected selection = signal<Formation | null>(this.formations()[1] ?? null);
protected totalPlaces = computed(() => this.formations().reduce((t, f) => t + f.places, 0));
}
3 formations disponibles.
1. HTML et CSS 🟢 débutant
2. Angular 🟠 intermédiaire
3. Flutter 🔴 avancé(dernière)
Total : 45 places
Sélection : Angular
@letdéclare une variable locale au template, en lecture seule, pour éviter de répéter une expression :@let total = totalPlaces();.- À l'intérieur de
@if (selection(); as f), TypeScript sait quefn'est pasnull: le rétrécissement de type vu dans les chapitres TypeScript s'applique aussi aux templates. - Remarque : dans du code plus ancien, vous rencontrerez
*ngIf,*ngForet[ngSwitch]. Ce sont les anciennes directives de contrôle de flux : les blocs@if,@foret@switchles remplacent, sont plus lisibles et ne demandent aucun import.
Les pipes
- Un pipe transforme une valeur pour l'affichage, sans modifier la donnée :
{{ valeur | pipe }}. Il peut recevoir des paramètres après:, et plusieurs pipes peuvent s'enchaîner. - Chaque pipe doit être importé dans
importsdu composant (DatePipe,CurrencyPipe,DecimalPipe… depuis@angular/common). Sinon :NG8004: No pipe found with name 'date'.
import { Component } from '@angular/core';
import {
CurrencyPipe, DatePipe, DecimalPipe, JsonPipe, LowerCasePipe,
PercentPipe, SlicePipe, TitleCasePipe, UpperCasePipe,
} from '@angular/common';
@Component({
selector: 'app-pipes',
imports: [UpperCasePipe, LowerCasePipe, TitleCasePipe, DatePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, JsonPipe],
template: `
<p>{{ nom | uppercase }}</p>
<p>{{ 'formation angular avancée' | titlecase }}</p>
<p>{{ debut | date }}</p>
<p>{{ debut | date: 'fullDate' }}</p>
<p>{{ debut | date: 'dd/MM/yyyy HH:mm' }}</p>
<p>{{ prix | currency: 'TND' }}</p>
<p>{{ prix | currency: 'EUR' }}</p>
<p>{{ moyenne | number: '1.2-2' }}</p>
<p>{{ taux | percent }}</p>
<p>{{ description | slice: 0 : 30 }}…</p>
<p>{{ debut | date: 'EEEE d MMMM' | uppercase }}</p>
<pre>{{ stagiaire | json }}</pre>
`,
})
export class Pipes {
protected nom = 'Amira Ben Ali';
protected debut = new Date(2026, 10, 3, 9, 30);
protected prix = 1480.5;
protected moyenne = 14.6666;
protected taux = 0.85;
protected description = 'Apprenez à créer des applications web modernes avec Angular et TypeScript.';
protected stagiaire = { nom: 'Amira', groupe: 'DEV1' };
}

- Le paramètre de
numbers'écrit'chiffresEntiersMin.décimalesMin-décimalesMax':'1.2-2'donne toujours exactement deux décimales. - Afficher en français: par défaut, Angular formate en anglais (
Nov 3, 2026,TND1,480.500). Pour obtenir3 nov. 2026et1 480,500 TND, on enregistre les données de la langue française et on la déclare comme langue de l'application danssrc/app/app.config.ts:
import { ApplicationConfig, LOCALE_ID, provideBrowserGlobalErrorListeners } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
registerLocaleData(localeFr);
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
{ provide: LOCALE_ID, useValue: 'fr' },
]
};
- Attention : déclarer
LOCALE_IDsans appelerregisterLocaleDataprovoque une erreur dans la console du navigateur :NG0701: Missing locale data for the locale "fr".
Aide-mémoire

-
Activité 1 : une carte de profil
- Dans le projet
premiers-pas-angular, générer le composantactivites/carte-profilet l'afficher dansapp.html(penser à l'ajouter dansimportsdeApp). Configurer la langue française. - Dans la classe, déclarer les données suivantes, puis écrire le template avec l'interpolation, des liaisons de propriété (
[src],[alt],[href]) et les pipesuppercase,date: 'longDate'etnumber: '1.2-2'.
- Dans le projet
protected stagiaire = {
prenom: 'Amira',
nom: 'Ben Ali',
groupe: 'DEV1',
photo: 'favicon.ico',
inscription: new Date(2026, 8, 15),
moyenne: 15.4567,
email: 'amira@exemple.tn',
};
- Résultat attendu : le lien ouvre la messagerie (
mailto:).

-
Activité 2 : un compteur de places
- Générer le composant
activites/compteur-placespour une session de 5 places:- un signal
reserveeset deuxcomputed:completettaux(pourcentage de remplissage) ; - les boutons Réserver et Annuler (liaisons d'événement), désactivés quand la session est complète ou vide (
[disabled]) ; - une jauge dont la largeur suit
[style.width.%]et qui devient rouge quand la session est complète ([class.plein]) ; - le message
N place(s) disponible(s), ouSession complèteen rouge.
- un signal
- Générer le composant

- Cliquer cinq fois sur Réserver : le bouton se désactive-t-il ? La jauge change-t-elle de couleur ? Que se passerait-il sans
[disabled]si l'on cliquait une sixième fois ?
Activité 3 : une liste de tâches
- Générer le composant
activites/liste-tacheset déclarer l'interfaceTache(id,texte,faite), puis un signaltaches(tableau vide au départ). Le template doit :- ajouter une tâche quand on appuie sur Entrée dans le champ (
#nouvelleet(keyup.enter)), puis vider le champ ; une saisie vide est ignorée ; - afficher les tâches avec
@for(track t.id), numérotées avec$index, etAucune tâche : profitez-en !grâce à@empty; - barrer une tâche cochée (
[class.faite],(change)) et la supprimer avec le bouton ✕ ; - afficher
N tâche(s) restante(s) sur Muniquement s'il y a au moins une tâche (@if,computed).
- ajouter une tâche quand on appuie sur Entrée dans le champ (
- Les méthodes
ajouter,basculeretsupprimerne modifient jamais le tableau : elles en créent un nouveau avecupdate, le spread,mapetfilter(chapitre « Destructuration, spread et rest »).

- Remplacer
track t.idpartrack $index: la liste fonctionne-t-elle toujours ? Pourquoit.idreste-t-il le meilleur choix ?
Activité 4 : un convertisseur de devises
- Générer le composant
activites/convertisseur, importerFormsModuleetCurrencyPipe, et déclarer :
type Devise = 'EUR' | 'USD' | 'GBP';
private readonly taux: Record<Devise, number> = { EUR: 0.29, USD: 0.32, GBP: 0.25 };
protected montant = signal(100);
protected devise = signal<Devise>('EUR');
protected resultat = computed(() => this.montant() * this.taux[this.devise()]);
- Lier un champ numérique à
montantet une liste déroulante (Euro, Dollar, Livre sterling) àdeviseavec[(ngModel)], puis afficher le montant en dinars et le résultat avec le pipecurrency. Au départ, la page affiche100,000 TND = 29,00 €; pour 250 en dollars :

- Pourquoi le dinar s'affiche-t-il avec trois décimales et l'euro avec deux, alors que vous n'avez rien précisé ?
Activité 5 : le statut des sessions
- Générer le composant
activites/statut-sessionavec le typeStatut = 'planifiee' | 'en-cours' | 'terminee' | 'annulee'et l'interfaceSession(id,titre,debut: Date,statut,inscrits). Déclarer trois sessions : Angular (3 novembre 2026, planifiée, 9 inscrits), TypeScript (20 octobre, en cours, 12) et HTML et CSS (1er septembre, terminée, 11). - Pour chaque session (
@for), calculer les places libres sur 12 avec@let, afficher la date avecdate: 'd MMM'et le statut avec@switch; seule une session planifiée affiche ses places libres. - Un bouton Annuler les sessions planifiées passe leur statut à
'annulee'.

- Après un clic sur le bouton, la première ligne doit devenir
Angular – 3 nov. – Annulée. - Remplacer le
@defaultpar@case ('annulee'), puis ajouter un statut'reportee'au type sans le traiter dans le@switch: la compilation signale-t-elle l'oubli ? Qu'affiche la page pour une session reportée ? - Faire un commit après chaque activité, puis un push :
git commit -m "Syntaxe Angular : activité N".
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG8001: 'app-carte-profil' is not a known element |
Composant utilisé dans un template sans être importé | L'ajouter dans imports du composant parent |
NG8113: CarteProfil is not used within the template of App (avertissement) |
Composant importé mais jamais utilisé dans le template | Ajouter sa balise <app-carte-profil />, ou le retirer de imports |
NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. |
FormsModule non importé |
imports: [FormsModule] |
NG8004: No pipe found with name 'uppercase'. |
Pipe non importé | Importer UpperCasePipe (ou le pipe concerné) depuis @angular/common |
TS2339: Property 'titr' does not exist on type 'App'. |
Faute de frappe dans le template | Corriger le nom : le template est vérifié comme du TypeScript |
NG8109: compteur is a function and should be invoked: compteur() (avertissement) |
Signal lu sans parenthèses | Écrire {{ compteur() }} |
TS2322: Type 'number' is not assignable to type 'WritableSignal<number>'. |
Affectation directe d'un signal : (click)="compteur = 5" |
compteur.set(5) ou compteur.update(…) |
NG5002: Parser Error: Bindings cannot contain assignments |
Affectation dans une interpolation ou une liaison de propriété | Déplacer la modification dans une méthode appelée par un événement |
NG5002: @for loop must have a "track" expression |
track oublié |
@for (x of liste; track x.id) |
NG5002: Unclosed block "if" |
Accolade fermante oubliée | Fermer chaque bloc @if { … }, @for { … } |
TS2339: Property 'value' does not exist on type 'EventTarget'. + TS2531: Object is possibly 'null'. |
$event.target.value sur un élément qui n'est pas un champ (bouton, div…) |
Utiliser une variable de référence #champ et champ.value |
NG0701: Missing locale data for the locale "fr". (console) |
LOCALE_ID déclaré sans registerLocaleData |
Appeler registerLocaleData(localeFr) dans app.config.ts |
TS2339: Property 'Logo' does not exist on type 'Carte'. pour [alt]="Logo" |
Entre crochets, la valeur est une expression : Logo est lu comme une propriété de la classe |
[alt]="'Logo'" ou simplement alt="Logo" |
-
À retenir
- Un composant = une classe décorée par
@Component(selector,imports, template, styles) ; les membresprotectedsont accessibles au template. {{ expr }}affiche,[prop]="expr"règle une propriété ([attr.],[class.],[style.]),(evenement)="instr"réagit,[(ngModel)]synchronise (avecFormsModule).- Un signal se lit avec
()et se modifie avecset()/update(). #nomdésigne un élément du template ;$eventest l'événement.@if/@else,@for(avectracket@empty),@switch/@caseet@letstructurent le template.- Les pipes (
date,currency,number…) formatent l'affichage ; on les importe, etLOCALE_ID+registerLocaleDatales passent en français.
- Un composant = une classe décorée par
Accolades pour afficher, crochets pour envoyer, parenthèses pour écouter, banane dans une boîte pour les deux : avec ces quatre symboles et les blocs @, vous savez lire n'importe quel template Angular.