À propos de ce cours
Angular utilise des décorateurs pour savoir ce qu’est chaque classe et comment ses membres communiquent. Ce chapitre présente les décorateurs de classe : @Component et ses métadonnées, @Directive, @Pipe, le nouveau @Service d’Angular 22 et @Injectable. Vous créerez une directive, un pipe et un service partagé. Vous étudierez ensuite les décorateurs de membres (@Input, @Output, @ViewChild, @HostListener, @Inject) et leurs remplaçants modernes basés sur les signals : input(), output(), model(), viewChild(), host et inject(). Vous apprendrez enfin à moderniser un ancien composant grâce aux migrations automatiques d’Angular, au fil de cinq activités pratiques.
🎬 La leçon en vidéo (11 min 45)
La vidéo ne s'affiche pas ? La regarder sur YouTube.
-
Objectifs
- Connaître les décorateurs de classe d'Angular :
@Component,@Directive,@Pipe,@Service,@Injectable,@NgModule - Configurer un composant avec les métadonnées de
@Component - Créer une directive d'attribut et un pipe personnalisés
- Créer un service avec
@Serviceet l'utiliser avecinject() - Comprendre les décorateurs de membres (
@Input,@Output,@ViewChild,@HostListener,@Inject…) et leurs équivalents actuels (input(),output(),model(),viewChild(),host,inject()) - Moderniser un ancien composant avec les migrations automatiques d'Angular
- Connaître les décorateurs de classe d'Angular :
-
Présentation
- Dans le chapitre « Présentation des décorateurs TypeScript », vous avez vu qu'un décorateur est une fonction placée avec
@devant une classe ou un membre. Angular s'appuie sur ce mécanisme pour savoir ce qu'est chaque classe et comment ses membres communiquent avec les templates et les autres classes. - Ses décorateurs se rangent en deux familles :
- les décorateurs de classe indiquent le rôle d'une classe : composant, directive, pipe, service… Ils restent au cœur de tout projet Angular ;
- les décorateurs de membres relient une propriété ou une méthode à un template, à un événement ou à l'injection de dépendances. Ils sont progressivement remplacés par des fonctions basées sur les signals, plus simples et mieux typées.
- Tous sont des fabriques: on les écrit toujours avec des parenthèses,
@Component({ … }),@Input(). Ils s'importent depuis@angular/core.
- Dans le chapitre « Présentation des décorateurs TypeScript », vous avez vu qu'un décorateur est une fonction placée avec

- Nouveautés d'Angular 22 : un nouveau décorateur
@Service()simplifie la création des services (ng generate servicele produit désormais à la place de@Injectable), et la stratégie de rafraîchissementOnPushest devenue la stratégie par défaut des composants. Les exemples de ce chapitre ont été testés avec Angular 22. - Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/decorateurs/.
@Component et ses métadonnées
@Componentreçoit un objet de métadonnées: le compilateur d'Angular le lit pour fabriquer le composant. Vous connaissez déjàselector,imports,templateetstyles; d'autres options règlent l'élément hôte, les services ou le rafraîchissement.

| Option | Rôle | Valeur par défaut |
|---|---|---|
selector |
Balise ('app-boutique'), attribut ('[appX]') ou classe ('.x') qui affiche le composant |
aucune (presque toujours renseigné) |
imports |
Composants, directives et pipes utilisés dans le template | [] |
template / templateUrl |
Template écrit dans le décorateur, ou fichier HTML | l'un des deux est obligatoire |
styles / styleUrl / styleUrls |
Styles CSS du composant, écrits ou dans un ou plusieurs fichiers | aucun style |
host |
Classes, attributs, propriétés et événements de l'élément hôte (la balise <app-boutique> elle-même) |
{} |
providers |
Services créés pour ce composant et ses enfants (une instance par composant) | [] |
changeDetection |
Quand Angular revérifie le composant : OnPush (quand un signal lu change, un événement survient ou une entrée change) ou Eager (à chaque passage) |
OnPush depuis Angular 22 |
encapsulation |
Portée des styles : Emulated (limités au composant), ShadowDom ou None (globaux) |
Emulated |
import { ChangeDetectionStrategy, Component, ViewEncapsulation, inject, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { Article, PanierService } from './panier.service';
import { CarteArticle } from './carte-article';
import { Surligner } from './surligner';
import { DureePipe } from './duree.pipe';
@Component({
selector: 'app-boutique',
imports: [CarteArticle, Surligner, DureePipe, CurrencyPipe],
templateUrl: './boutique.html',
styleUrl: './boutique.css',
host: { class: 'boutique', '(window:keydown.escape)': 'panier.vider()' },
changeDetection: ChangeDetectionStrategy.OnPush, // valeur par défaut, écrite ici pour l'exemple
encapsulation: ViewEncapsulation.Emulated, // valeur par défaut, écrite ici pour l'exemple
})
export class Boutique {
protected readonly panier = inject(PanierService);
protected readonly quantiteClavier = signal(2);
protected readonly catalogue: Article[] = [
{ id: 1, nom: 'Clavier', prix: 45 },
{ id: 2, nom: 'Souris', prix: 20 },
];
protected ajouter(article: Article): void {
this.panier.ajouter(article);
}
}
- Grâce à
host, la balise<app-boutique>reçoit la classe CSSboutique, et un appui sur Échap n'importe où dans la page vide le panier. - Remarque : avec
OnPush, Angular ne revérifie un composant que lorsqu'il a une bonne raison de le faire. Si vos données qui changent sont des signals, comme dans ce cours, vous n'avez rien à faire. Dans un ancien projet mis à jour,ng updateajoutechangeDetection: ChangeDetectionStrategy.Eageraux composants existants pour conserver leur comportement (Eagerest le nouveau nom de l'ancienne stratégieDefault).
@Directive: une directive d'attribut
- Une directive ajoute un comportement à un élément existant, sans template. Son
selectorest en général un attribut, écrit entre crochets :'[appSurligner]'. - Elle agit sur son élément hôte avec la propriété
host:'[propriete]'lie une propriété,'(evenement)'écoute un événement. - Pour la créer :
npx ng generate directive decorateurs/surligner.

import { Directive, computed, input, signal } from '@angular/core';
@Directive({
selector: '[appSurligner]',
host: {
'[style.backgroundColor]': 'actif() ? fond() : null',
'(mouseenter)': 'actif.set(true)',
'(mouseleave)': 'actif.set(false)',
},
})
export class Surligner {
readonly couleur = input('', { alias: 'appSurligner' });
protected readonly fond = computed(() => this.couleur() || '#fff3a3');
protected readonly actif = signal(false);
}
<p appSurligner>Survolez-moi (jaune par défaut)</p>
<p appSurligner="#c7f9cc">Survolez-moi (vert)</p>
alias: 'appSurligner'permet de passer la couleur directement dans l'attribut de la directive.- Attention au piège de l'attribut vide : sur
<p appSurligner>, l'entrée reçoit une chaîne vide'', qui remplace la valeur par défaut deinput(). Avecinput('#fff3a3', …), le fond serait donc vide ! C'est pourquoi l'exemple utilisethis.couleur() || '#fff3a3'. - Voici la même directive dans l'ancien style, que vous rencontrerez dans de nombreux projets :
@Input,@HostBindinget@HostListener.
import { Directive, HostBinding, HostListener, Input } from '@angular/core';
@Directive({ selector: '[appSurlignerAncien]' })
export class SurlignerAncien {
@Input('appSurlignerAncien') couleur = '';
@HostBinding('style.backgroundColor') fond: string | null = null;
@HostListener('mouseenter') entrer(): void {
this.fond = this.couleur || '#fff3a3';
}
@HostListener('mouseleave') sortir(): void {
this.fond = null;
}
}
-
@Pipe: un pipe personnalisé- Un pipe est une classe décorée par
@Pipe({ name: '…' })qui implémente l'interfacePipeTransform: sa méthodetransformreçoit la valeur à gauche du|, puis les paramètres écrits après:. - Pour le créer :
npx ng generate pipe decorateurs/duree.
- Un pipe est une classe décorée par
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'duree' })
export class DureePipe implements PipeTransform {
transform(minutes: number, format: 'court' | 'long' = 'court'): string {
const h = Math.floor(minutes / 60);
const m = minutes % 60;
if (format === 'long') {
return [h ? `${h} heure${h > 1 ? 's' : ''}` : '', m ? `${m} minute${m > 1 ? 's' : ''}` : '']
.filter(Boolean)
.join(' et ');
}
return h ? `${h} h ${String(m).padStart(2, '0')}` : `${m} min`;
}
}
<p>{{ 90 | duree }} – {{ 2400 | duree }} – {{ 45 | duree }} – {{ 150 | duree: 'long' }}</p>
<!-- 1 h 30 – 40 h 00 – 45 min – 2 heures et 30 minutes -->
- Les types de
transformsont vérifiés dans le template :{{ 'abc' | duree }}est refusé (TS2345), comme un paramètre en trop (TS2554: Expected 1 arguments, but got 2pour un pipe sans paramètre). - Un pipe est pur par défaut : Angular ne rappelle
transformque si la valeur ou un paramètre change, ce qui le rend très économique.
@Service et @Injectable: les services
- Un service est une classe qui contient des données ou un traitement partagés par plusieurs composants : un panier, un appel à une API, l'utilisateur connecté…
- Depuis Angular 22, on le déclare avec
@Service(). Angular crée alors une seule instance (un singleton), à la première demande, et la partage dans toute l'application. Un composant l'obtient avec la fonctioninject(). - Pour le créer :
npx ng generate service decorateurs/panier.
import { Service, computed, inject, signal } from '@angular/core';
import { TAUX_TVA } from './tokens';
export interface Article {
id: number;
nom: string;
prix: number;
}
@Service()
export class PanierService {
private readonly tva = inject(TAUX_TVA);
readonly articles = signal<Article[]>([]);
readonly totalHT = computed(() => this.articles().reduce((t, a) => t + a.prix, 0));
readonly totalTTC = computed(() => this.totalHT() * (1 + this.tva));
ajouter(article: Article): void {
this.articles.update((liste) => [...liste, article]);
}
vider(): void {
this.articles.set([]);
}
}

@Service()est un raccourci de l'écriture classique@Injectable({ providedIn: 'root' }), que vous trouverez dans tous les projets antérieurs à Angular 22.@Serviceimpose d'obtenir ses dépendances avecinject(): l'injection par le constructeur est refusée à la compilation (NG2028).@Service({ autoProvided: false })crée un service qui n'est pas disponible automatiquement : il faut le déclarer dansprovidersd'un composant, qui en recevra sa propre instance.
| Besoin | Décorateur |
|---|---|
Nouveau service partagé, dépendances avec inject() |
@Service() |
Service propre à un composant (déclaré dans providers) |
@Service({ autoProvided: false }) |
Injection par le constructeur, providedIn: 'platform', fabriques avancées (useClass, useFactory…) |
@Injectable({ … }) |
| Projet en Angular 21 ou antérieur | @Injectable({ providedIn: 'root' }) |
-
Les entrées et sorties :
@Input,@Output→input(),output(),model()- Un composant enfant reçoit des données de son parent par ses entrées, et le prévient de ce qui se passe par ses sorties. Dans le template du parent, on lie une entrée avec
[ ]et on écoute une sortie avec( ).
- Un composant enfant reçoit des données de son parent par ses entrées, et le prévient de ce qui se passe par ses sorties. Dans le template du parent, on lie une entrée avec

- Ancien style: les décorateurs
@Input()et@Output(), avec unEventEmitter.
import { Component, EventEmitter, Input, Output, booleanAttribute } from '@angular/core';
import { Article } from './panier.service';
@Component({
selector: 'app-carte-ancienne',
template: `
<article [class.vedette]="vedette">
<h3>{{ article.nom }}</h3>
<p>{{ article.prix }} DT</p>
<button (click)="choisir.emit(article)">Ajouter</button>
</article>
`,
})
export class CarteAncienne {
@Input({ required: true }) article!: Article;
@Input({ transform: booleanAttribute }) vedette = false;
@Output() choisir = new EventEmitter<Article>();
}
- Style actuel: des fonctions qui créent des signals.
input()est en lecture seule pour l'enfant ;output()remplaceEventEmitter;model()est une entrée modifiable qui permet la liaison bidirectionnelle[( )].
import { Component, booleanAttribute, input, model, output } from '@angular/core';
import { Article } from './panier.service';
@Component({
selector: 'app-carte-article',
template: `
<article [class.vedette]="vedette()">
<h3>{{ article().nom }}</h3>
<p>{{ article().prix }} DT</p>
<label>Quantité <input type="number" min="1" [value]="quantite()" (input)="quantite.set(+$event.target.value)" /></label>
<button (click)="choisir.emit(article())">Ajouter</button>
</article>
`,
styles: `.vedette { border: 2px solid #f59e0b; }`,
})
export class CarteArticle {
readonly article = input.required<Article>();
readonly vedette = input(false, { transform: booleanAttribute });
readonly quantite = model(1);
readonly choisir = output<Article>();
}
<!-- dans le template de Boutique (le parent) -->
<app-carte-article [article]="catalogue[0]" vedette [(quantite)]="quantiteClavier" (choisir)="ajouter($event)" />
<app-carte-ancienne [article]="catalogue[1]" (choisir)="ajouter($event)" />
<p>Quantité de claviers (parent) : {{ quantiteClavier() }}</p>
<p>Panier : {{ panier.articles().length }} article(s) – {{ panier.totalHT() | currency: 'TND' }} HT – {{ panier.totalTTC() | currency: 'TND' }} TTC</p>
Quantité de claviers (parent) : 2
Panier : 0 article(s) – 0,000 TND HT – 0,000 TND TTC
(après avoir tapé 5 dans la quantité et cliqué sur les deux boutons Ajouter)
Quantité de claviers (parent) : 5
Panier : 2 article(s) – 65,000 TND HT – 77,350 TND TTC
required: true/input.required(): le parent est obligé de fournir l'entrée, sinon la compilation échoue (NG8008).transform: booleanAttributetransforme la présence de l'attributvedette(sans valeur) entrue.numberAttributefait de même pour les nombres.- Les deux composants s'utilisent de la même manière dans le template du parent : passer d'un style à l'autre ne change rien pour celui qui utilise le composant.
Les requêtes : @ViewChild → viewChild()
- Une requête donne à la classe accès à un élément de son template, repéré par une variable de référence
#nom: par exemple pour placer le curseur dans un champ.
// Ancien style : @ViewChild + ngAfterViewInit
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-recherche-ancienne',
template: `<input #champ placeholder="Rechercher" />`,
})
export class RechercheAncienne implements AfterViewInit {
@ViewChild('champ') champ!: ElementRef<HTMLInputElement>;
ngAfterViewInit(): void {
this.champ.nativeElement.focus();
}
}
// Style actuel : viewChild() + afterNextRender()
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
@Component({
selector: 'app-recherche',
template: `<input #champ placeholder="Rechercher une formation" />`,
})
export class Recherche {
private readonly champ = viewChild.required<ElementRef<HTMLInputElement>>('champ');
constructor() {
afterNextRender(() => this.champ().nativeElement.focus());
}
}
viewChild()renvoie un signal, qui vautundefinedtant que l'élément n'existe pas ;viewChild.required()garantit qu'il existe, mais ne doit être lu qu'après l'affichage du template (d'oùafterNextRender). Le lire trop tôt provoque l'erreurNG0951.contentChild()(ancien@ContentChild) fonctionne de la même façon pour le contenu projeté, c'est-à-dire ce que le parent place entre les balises du composant :
import { Component, ElementRef, computed, contentChild } from '@angular/core';
@Component({
selector: 'app-panneau',
template: `
<section class="panneau">
<ng-content select="[titre]" />
<ng-content />
<small>{{ resume() }}</small>
</section>
`,
})
export class Panneau {
private readonly titre = contentChild<ElementRef<HTMLElement>>('titre');
protected readonly resume = computed(() =>
this.titre() ? `Panneau « ${this.titre()!.nativeElement.textContent} »` : 'Panneau sans titre',
);
}
<app-panneau><h3 #titre titre>Inscriptions</h3><p>12 stagiaires inscrits.</p></app-panneau>
<!-- Inscriptions / 12 stagiaires inscrits. / Panneau « Inscriptions » -->
-
@HostListeneret@HostBinding→host@HostListener('evenement')exécute une méthode quand l'élément hôte (ouwindow,document) reçoit un événement ;@HostBinding('propriete')lie une propriété de l'hôte à un membre de la classe.- Angular recommande désormais la propriété
hostdu décorateur, déjà utilisée dansBoutiqueetSurligner:
| Ancien style | Propriété host |
|---|---|
@HostListener('mouseenter') entrer() { … } |
host: { '(mouseenter)': 'entrer()' } |
@HostListener('window:keydown.escape') vider() { … } |
host: { '(window:keydown.escape)': 'vider()' } |
@HostBinding('class.actif') actif = false; |
host: { '[class.actif]': 'actif()' } |
@HostBinding('attr.role') role = 'button'; |
host: { role: 'button' } (valeur fixe) |
- Attention : dans
host,$eventest un simpleEvent:$event.target.valuey est refusé (TS2339: Property 'value' does not exist on type 'EventTarget'). Pour lire la valeur d'un champ, injectez l'élément avecinject(ElementRef)(voir l'activité 1).
@Inject et l'injection par le constructeur → inject()
- Historiquement, une classe recevait ses services en paramètres de son constructeur. Pour une valeur qui n'est pas une classe (un nombre, une configuration…), on utilisait un jeton (
InjectionToken) et le décorateur de paramètre@Inject.
// tokens.ts : un jeton pour une valeur de configuration
import { InjectionToken } from '@angular/core';
export const TAUX_TVA = new InjectionToken<number>('Taux de TVA');
// app.config.ts : on fournit sa valeur
providers: [
{ provide: TAUX_TVA, useValue: 0.19 },
]
// Ancien style : @Injectable + constructeur + @Inject
import { Inject, Injectable, signal } from '@angular/core';
import { TAUX_TVA } from './tokens';
@Injectable({ providedIn: 'root' })
export class JournalService {
readonly lignes = signal<string[]>([]);
constructor(@Inject(TAUX_TVA) private tva: number) {}
ecrire(message: string): void {
this.lignes.update((l) => [...l, `${message} (TVA ${this.tva * 100} %)`]);
}
}
- Avec
inject(), la même dépendance s'écrit en une ligne, sans constructeur ni décorateur, et fonctionne pour un service comme pour un jeton :private readonly tva = inject(TAUX_TVA);(voirPanierService). - Attention :
inject()ne peut être appelé que pendant la création de la classe : dans l'initialisation d'une propriété ou dans le constructeur. Appelé plus tard, dans une méthode, il provoque l'erreurNG0203.
@NgModule: à savoir reconnaître
- Avant Angular 14, chaque composant devait être déclaré dans un module: une classe décorée par
@NgModulequi regroupe composants, directives, pipes et services. Depuis Angular 19, les composants sont autonomes (standalone) par défaut et importent eux-mêmes ce dont ils ont besoin : vous n'avez plus à écrire de module.
// Ancien code : vous le rencontrerez dans des projets existants
@NgModule({
declarations: [AppComponent, CarteComponent],
imports: [BrowserModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
- La migration
npx ng generate @angular/core:standaloneconvertit un projet à modules en composants autonomes.
Migrer automatiquement vers le style actuel
- Angular fournit des migrations qui réécrivent le code à votre place. On les lance avec
ng generate, en limitant éventuellement leur action à un dossier avec--path:

npx ng generate @angular/core:inject-migration --path src/app/migration
npx ng generate @angular/core:signal-input-migration --path src/app/migration
npx ng generate @angular/core:output-migration --path src/app/migration
npx ng generate @angular/core:signal-queries-migration --path src/app/migration
npx ng generate @angular/core:service-migration --path src/app/migration
# ou, pour les entrées, sorties et requêtes en une seule fois :
npx ng generate @angular/core:signals
- Les migrations sont prudentes: elles ne convertissent que ce qu'elles peuvent transformer sans risque. Sur l'ancien composant de l'activité 5, elles affichent par exemple :
Successfully migrated to signal inputs 🎉
-> Migrated 1/2 inputs.
To see why 1 inputs couldn't be migrated
consider re-running with "--insert-todos" or "--best-effort-mode".
- L'entrée
notesn'a pas été convertie car la classe la modifie (this.notes = …), ce qu'uninput()en lecture seule ne permet pas. De même, aucune migration ne convertit@HostListeneret@HostBinding: ces cas se terminent à la main. - Conseil : faites un commit avant de lancer une migration, puis relisez les modifications avec
git diff.
Activité 1 : une directive appLongueurMax
- Générer la directive
decorateurs/longueur-max, de sélecteur'input[appLongueurMax], textarea[appLongueurMax]'(elle ne s'applique qu'aux champs de saisie). Elle doit :- recevoir la longueur maximale par son attribut :
appLongueurMax="10", avec 20 par défaut quand l'attribut est vide (input()avecaliasettransform) ; - compter les caractères à chaque saisie, en lisant la valeur de l'élément obtenu par
inject(ElementRef); - passer la bordure en rouge et l'attribut
aria-invalidàtrueau-delà du maximum, et afficherN / max caractèresdans l'info-bulle (title), tout cela avechost.
- recevoir la longueur maximale par son attribut :
<label>Pseudo (10 max) <input appLongueurMax="10" placeholder="Pseudo" /></label>
<label>Message (20 par défaut) <textarea appLongueurMax></textarea></label>

- Avec
transform: numberAttribute, l'info-bulle du message affiche25 / NaN caractères: expliquer pourquoi, puis corriger avec la valeur de repli denumberAttribute(valeur, 20). - Placer la directive sur un
<p appLongueurMax>: que se passe-t-il, et pourquoi ?
Activité 2 : deux pipes, initiales et mention
- Générer les pipes
decorateurs/initialesetdecorateurs/mention:initialestransforme un nom complet en initiales, suivies d'un séparateur ('.'par défaut), en ignorant les espaces superflus ; il renvoie''pournullouundefined;mentiontransforme une moyenne en mention :Très bien(16),Bien(14),Assez bien(12),Passable(10), sinonAjourné.
<p>{{ 'Amira Ben Ali' | initiales }}</p>
<p>{{ ' sami trabelsi ' | initiales: '' }}</p>
<p>{{ null | initiales }}</p>
@for (m of [17.5, 15, 12.25, 10, 8]; track $index) {
<p>{{ m }} : {{ m | mention }}</p>
}

- Écrire
{{ 15 | initiales }}: quelle erreur obtenez-vous ? Que vous apprend-elle sur la vérification des pipes ?
Activité 3 : une note en étoiles avec model()
- Générer le composant
decorateurs/note-etoiles, qui affiche une note sous forme d'étoiles cliquables :valeur = model(0): la note, modifiable par le parent et par un clic sur une étoile ;max = input(5, …)avecnumberAttribute, etlectureSeule = input(false, …)avecbooleanAttribute(étoiles désactivées) ;noteValidee = output<number>(), émis à chaque clic ;- les étoiles sont générées avec
@forà partir d'uncomputed([1, 2, …, max]) ; une étoile pleine est orange ([class.pleine]).
- Dans le parent, déclarer
note = signal(3)etmessage = signal(''), puis :
<app-note-etoiles [(valeur)]="note" (noteValidee)="message.set('Merci pour votre note de ' + $event + ' !')" />
<app-note-etoiles [valeur]="4" max="10" lectureSeule />
<p>Note dans le parent : {{ note() }}</p>
<p>{{ message() }}</p>
<button type="button" (click)="note.set(0)">Réinitialiser</button>

- Après un clic sur la 4e étoile, la note du parent vaut 4 ; un clic sur Réinitialiser vide les étoiles du premier composant. Expliquer en commentaire pourquoi cela prouve que la liaison fonctionne dans les deux sens.
- Remplacer
model(0)parinput(0): quelle erreur obtenez-vous danschoisir()? Et dans le template du parent ?
Activité 4 : un service de favoris partagé
- Générer le service
decorateurs/favoris(@Service()) : un signal privéids(tableau de nombres), uncomputednombre, et les méthodesestFavori(id)etbasculer(id)(ajoute ou retire l'id, sans modifier le tableau existant). - Générer deux composants indépendants qui l'obtiennent avec
inject():liste-formationsaffiche Angular, TypeScript et Flutter avec un bouton☆ Ajouter/★ Retirer;compteur-favorisaffiche le badge★ N favori(s).

- Pourquoi le compteur se met-il à jour alors que les deux composants ne communiquent pas entre eux ?
- Remplacer
@Service()par@Service({ autoProvided: false }): noter l'erreur affichée dans la console du navigateur. Ajouter ensuiteproviders: [FavorisService]dans chacun des deux composants : le compteur fonctionne-t-il encore ? Expliquer.
Activité 5 : moderniser un ancien composant
- Créer le dossier
src/app/migration/avec ce service et ce composant écrits dans l'ancien style, puis les afficher avec<app-migration nom="Sami" [notes]="[12]" (moyenneChange)="derniere.set($event)" />. Faire un commit.
// notes.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class NotesService {
moyenne(notes: number[]): number {
return notes.reduce((s, n) => s + n, 0) / notes.length;
}
}
// migration.ts
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, Output, ViewChild } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { NotesService } from './notes.service';
@Component({
selector: 'app-migration',
imports: [DecimalPipe],
template: `
<h3>{{ nom }}</h3>
<input #note type="number" />
<button (click)="ajouter(note.valueAsNumber)">Ajouter</button>
<p>Moyenne : {{ moyenne | number: '1.2-2' }}</p>
`,
})
export class Migration implements AfterViewInit {
@Input() nom = 'Amira';
@Input() notes: number[] = [];
@Output() moyenneChange = new EventEmitter<number>();
@ViewChild('note') champ!: ElementRef<HTMLInputElement>;
moyenne = 0;
constructor(private notesService: NotesService) {}
ngAfterViewInit(): void {
this.champ.nativeElement.focus();
}
@HostListener('window:keydown.escape')
effacer(): void {
this.champ.nativeElement.value = '';
}
ajouter(note: number): void {
this.notes = [...this.notes, note];
this.moyenne = this.notesService.moyenne(this.notes);
this.moyenneChange.emit(this.moyenne);
}
}
- Lancer les cinq migrations de la section « Migrer automatiquement » sur ce dossier, puis lire le résultat avec
git diff: qu'est-ce qui a été converti, et qu'est-ce qui ne l'a pas été ? - Terminer à la main, pour qu'il ne reste aucun décorateur de membre :
notesdevient uninput<number[]>([]), et la liste modifiable unlinkedSignal(() => this.notes())(un signal qui repart de la valeur de l'entrée et que l'on peut modifier) ;moyennedevient uncomputed(0 si la liste est vide) ;@HostListenerdevient une entrée dehost, etngAfterViewInitunafterNextRender;ajouterignore une note invalide (hors de 0 à 20), et le template affiche aussi la liste des notes.

- Avant la modernisation, la page affichait
Moyenne : 0,00au départ, alors quenotesvaut[12]: pourquoi ? Pourquoi la version modernisée affiche-t-elle12,00? - Faire un commit après chaque activité, puis un push :
git commit -m "Décorateurs Angular : activité N".
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG8008: Required input 'article' from component CarteArticle must be specified. |
Entrée obligatoire non fournie par le parent | Ajouter [article]="…", ou rendre l'entrée facultative avec une valeur par défaut |
TS2339: Property 'set' does not exist on type 'InputSignal<string>'. |
Modification d'un input(), en lecture seule dans l'enfant |
Utiliser model(), ou une copie modifiable (linkedSignal) |
NG8007: The property and event halves of the two-way binding 'valeur' are not bound to the same target. |
[( )] utilisé sur une entrée créée avec input() |
Déclarer l'entrée avec model() |
TS2322: Type 'string' is not assignable to type 'number'. sur [quantite]="'trois'" |
Valeur du mauvais type passée à une entrée | Passer une valeur du bon type, ou ajouter un transform |
NG2028: @Service class cannot use constructor dependency injection. Use the `inject` function instead. |
Paramètres de constructeur dans une classe @Service |
Remplacer par private readonly x = inject(X);, ou garder @Injectable |
NG0201: No provider found for `_StockService`. (console) |
Service @Service({ autoProvided: false }) ou @Injectable() non déclaré |
L'ajouter dans providers du composant, ou utiliser @Service() |
NG0203: … `inject()` function must be called from an injection context … (console) |
inject() appelé dans une méthode, après la création de la classe |
Appeler inject() dans l'initialisation d'une propriété ou dans le constructeur |
NG0951: Child query result is required but no value is available. (console) |
viewChild.required() lu avant l'affichage, ou élément absent (dans un @if faux) |
Le lire dans afterNextRender, ou utiliser viewChild() et tester undefined |
NG8113: Gras is not used within the template of Erreurs (avertissement) |
Sélecteur de directive écrit sans crochets : 'appGras' désigne une balise <appGras> |
Écrire selector: '[appGras]' |
TS2339: Property 'value' does not exist on type 'EventTarget'. dans host |
Dans host, $event n'est pas typé selon l'élément |
Lire la valeur avec inject(ElementRef) |
TS2345 / TS2554 sur un pipe personnalisé |
Valeur ou paramètres qui ne correspondent pas à transform |
Respecter la signature de transform |
Valeur par défaut ignorée sur <p appSurligner>, ou NaN avec numberAttribute |
Un attribut sans valeur transmet une chaîne vide '' |
valeur || défaut, ou numberAttribute(valeur, défaut) |
| Un ancien composant ne se met plus à jour après la mise à niveau | Propriétés ordinaires modifiées hors événement, avec OnPush par défaut |
Utiliser des signals, ou changeDetection: ChangeDetectionStrategy.Eager |
-
À retenir
- Les décorateurs de classe disent ce qu'est une classe :
@Component(composant),@Directive(comportement sur un élément),@Pipe(transformation),@Service/@Injectable(service),@NgModule(ancien). @Componentse configure avec ses métadonnées :selector,imports, template, styles,host,providers,changeDetection(OnPushpar défaut),encapsulation.- Depuis Angular 22,
@Service()déclare un service partagé, obtenu avecinject();@Injectablereste utile pour l'injection par constructeur et les cas avancés. - Les décorateurs de membres ont des équivalents modernes :
@Input→input()/model(),@Output→output(),@ViewChild/@ContentChild→viewChild()/contentChild(),@HostListener/@HostBinding→host,@Inject→inject(). - Les deux styles coexistent ; les migrations
ng generate @angular/core:…convertissent la plupart des cas, le reste se termine à la main.
- Les décorateurs de classe disent ce qu'est une classe :
Les décorateurs de classe disent à Angular qui est qui ; les fonctions modernes disent comment les données circulent. Retenez les premiers, préférez les secondes, et sachez lire l'ancien style : vous le croiserez longtemps.