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

Les décorateurs Angular

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, @Pi...

0 chapitres 0 QCM Accès gratuit
Dans cet article

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

  1. 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 @Service et l'utiliser avec inject()
    • 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
  2. 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.

Les décorateurs d'Angular : décorateurs de classe et décorateurs de membres

  • Nouveautés d'Angular 22 : un nouveau décorateur @Service() simplifie la création des services (ng generate service le produit désormais à la place de @Injectable), et la stratégie de rafraîchissement OnPush est 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-angular et créez un dossier src/app/decorateurs/.
  • @Component et ses métadonnées

    • @Component reçoit un objet de métadonnées: le compilateur d'Angular le lit pour fabriquer le composant. Vous connaissez déjà selector, imports, template et styles; d'autres options règlent l'élément hôte, les services ou le rafraîchissement.

    Les métadonnées de @Component

    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 CSS boutique, 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 update ajoute changeDetection: ChangeDetectionStrategy.Eager aux composants existants pour conserver leur comportement (Eager est le nouveau nom de l'ancienne stratégie Default).
  • @Directive: une directive d'attribut

    • Une directive ajoute un comportement à un élément existant, sans template. Son selector est 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.

    Une directive d'attribut et sa propriété host

    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 de input(). Avec input('#fff3a3', …), le fond serait donc vide ! C'est pourquoi l'exemple utilise this.couleur() || '#fff3a3'.
    • Voici la même directive dans l'ancien style, que vous rencontrerez dans de nombreux projets : @Input, @HostBinding et @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;
      }
    }
    1. @Pipe: un pipe personnalisé

      • Un pipe est une classe décorée par @Pipe({ name: '…' }) qui implémente l'interface PipeTransform: sa méthode transform reçoit la valeur à gauche du |, puis les paramètres écrits après :.
      • Pour le créer : npx ng generate pipe decorateurs/duree.
    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 transform sont vérifiés dans le template : {{ 'abc' | duree }} est refusé (TS2345), comme un paramètre en trop (TS2554: Expected 1 arguments, but got 2 pour un pipe sans paramètre).
    • Un pipe est pur par défaut : Angular ne rappelle transform que 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 fonction inject().
    • 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 et inject() : un service partagé

    • @Service() est un raccourci de l'écriture classique @Injectable({ providedIn: 'root' }), que vous trouverez dans tous les projets antérieurs à Angular 22.
    • @Service impose d'obtenir ses dépendances avec inject(): 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 dans providers d'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' })
    1. 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 ( ).

    input(), output() et model() entre un parent et un enfant

    • Ancien style: les décorateurs @Input() et @Output(), avec un EventEmitter.
    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() remplace EventEmitter; 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: booleanAttribute transforme la présence de l'attribut vedette (sans valeur) en true. numberAttribute fait 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 vaut undefined tant 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'erreur NG0951.
    • 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 » -->
    1. @HostListener et @HostBinding → host

      • @HostListener('evenement') exécute une méthode quand l'élément hôte (ou window, 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é host du décorateur, déjà utilisée dans Boutique et Surligner:
    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, $event est un simple Event: $event.target.value y est refusé (TS2339: Property 'value' does not exist on type 'EventTarget'). Pour lire la valeur d'un champ, injectez l'élément avec inject(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); (voir PanierService).
    • 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'erreur NG0203.
  • @NgModule: à savoir reconnaître

    • Avant Angular 14, chaque composant devait être déclaré dans un module: une classe décorée par @NgModule qui 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:standalone convertit 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:

    De l'ancien style au style actuel : tableau de correspondance et migrations

    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 notes n'a pas été convertie car la classe la modifie (this.notes = …), ce qu'un input() en lecture seule ne permet pas. De même, aucune migration ne convertit @HostListener et @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() avec alias et transform) ;
      • 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 à true au-delà du maximum, et afficher N / max caractères dans l'info-bulle (title), tout cela avec host.
    <label>Pseudo (10 max) <input appLongueurMax="10" placeholder="Pseudo" /></label>
    <label>Message (20 par défaut) <textarea appLongueurMax></textarea></label>

    Résultat attendu de l'activité 1 : bordure rouge au-delà de la longueur maximale

    • Avec transform: numberAttribute, l'info-bulle du message affiche 25 / NaN caractères: expliquer pourquoi, puis corriger avec la valeur de repli de numberAttribute(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/initiales et decorateurs/mention:
      • initiales transforme un nom complet en initiales, suivies d'un séparateur ('.' par défaut), en ignorant les espaces superflus ; il renvoie '' pour null ou undefined;
      • mention transforme une moyenne en mention : Très bien (16), Bien (14), Assez bien (12), Passable (10), sinon Ajourné.
    <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>
    }

    Résultat attendu de l'activité 2 : pipes initiales et mention

    • É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, …) avec numberAttribute, et lectureSeule = input(false, …) avec booleanAttribute (étoiles désactivées) ;
      • noteValidee = output<number>(), émis à chaque clic ;
      • les étoiles sont générées avec @for à partir d'un computed ([1, 2, …, max]) ; une étoile pleine est orange ([class.pleine]).
    • Dans le parent, déclarer note = signal(3) et message = 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>

    Résultat attendu de l'activité 3 après un clic sur la quatrième étoile

    • 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) par input(0): quelle erreur obtenez-vous dans choisir()? 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), un computed nombre, et les méthodes estFavori(id) et basculer(id) (ajoute ou retire l'id, sans modifier le tableau existant).
    • Générer deux composants indépendants qui l'obtiennent avec inject():
      • liste-formations affiche Angular, TypeScript et Flutter avec un bouton ☆ Ajouter / ★ Retirer;
      • compteur-favoris affiche le badge ★ N favori(s).

    Résultat attendu de l'activité 4 : deux composants qui partagent un service

    • 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 ensuite providers: [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 :
      • notes devient un input<number[]>([]), et la liste modifiable un linkedSignal(() => this.notes()) (un signal qui repart de la valeur de l'entrée et que l'on peut modifier) ;
      • moyenne devient un computed (0 si la liste est vide) ;
      • @HostListener devient une entrée de host, et ngAfterViewInit un afterNextRender;
      • ajouter ignore une note invalide (hors de 0 à 20), et le template affiche aussi la liste des notes.

    Résultat attendu de l'activité 5 après l'ajout des notes 14 et 16

    • Avant la modernisation, la page affichait Moyenne : 0,00 au départ, alors que notes vaut [12]: pourquoi ? Pourquoi la version modernisée affiche-t-elle 12,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
    1. À 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).
      • @Component se configure avec ses métadonnées : selector, imports, template, styles, host, providers, changeDetection (OnPush par défaut), encapsulation.
      • Depuis Angular 22, @Service() déclare un service partagé, obtenu avec inject(); @Injectable reste 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 à 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.