Composants, templates et signaux

Première leçon du cours Angular, en accès libre : environ 18 minutes, 4 exercices et 3 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Créer un projet avec Angular CLI et comprendre la structure d'une application standalone
  • Écrire un composant avec son template : interpolation, liaisons de propriété et d'événement, @if et @for
  • Gérer un état local avec les signaux et transmettre des données avec input() et output()

Composants standaloneTemplates et blocs de contrôleSignaux (signal, computed)input() et output()

Créer un projet Angular

Angular est un framework complet, maintenu par Google, écrit en TypeScript : routeur, formulaires, client HTTP, tests et outil en ligne de commande sont fournis d’origine. On démarre avec l’Angular CLI :

npm install -g @angular/cli   # Node.js 22.22, 24.15 ou 26 (LTS conseillée)
ng new boutique          # questions : feuille de style, rendu serveur, outils d'IA…
cd boutique
ng serve                 # http://localhost:4200, rechargement automatique
ng generate component liste-taches   # raccourci : ng g c liste-taches

Le projet créé aujourd’hui utilise Angular 22.2 avec la version de TypeScript qu’il prend en charge (6.0) ; il fonctionne sans zone.js (détection des changements pilotée par les signaux et les événements) et ses tests unitaires (ng test) tournent avec Vitest. Les fichiers ne portent plus de suffixe : le composant racine est la classe App de src/app/app.ts.

Le point d’entrée src/main.ts démarre l’application avec bootstrapApplication(App, appConfig) ; app.config.ts déclare les fournisseurs globaux (routeur, client HTTP).

Historiquement, les composants étaient regroupés dans des NgModule. Les versions récentes rendent les composants standalone par défaut : chaque composant déclare lui-même ce qu’il utilise dans imports. Vous rencontrerez encore des NgModule dans les projets existants ; le principe des composants reste le même.

Anatomie d’un composant

import { Component, signal, computed } from '@angular/core';

interface Tache { id: number; libelle: string; faite: boolean }

@Component({
  selector: 'app-liste-taches',
  template: `
    <h2>{{ titre }}</h2>
    <ul>
      @for (tache of taches(); track tache.id) {
        <li [class.faite]="tache.faite">
          <input type="checkbox" [checked]="tache.faite" (change)="basculer(tache.id)" />
          {{ tache.libelle }}
        </li>
      } @empty {
        <li>Aucune tâche.</li>
      }
    </ul>
    @if (restantes() > 0) {
      <p>{{ restantes() }} tâche(s) restante(s)</p>
    } @else {
      <p>Tout est fait.</p>
    }
  `,
  styles: `.faite { text-decoration: line-through; }`,
})
export class ListeTaches {
  titre = 'Mes tâches';
  taches = signal<Tache[]>([
    { id: 1, libelle: 'Acheter du pain', faite: false },
    { id: 2, libelle: 'Réviser Angular', faite: true },
  ]);
  restantes = computed(() => this.taches().filter((t) => !t.faite).length);

  basculer(id: number) {
    this.taches.update((liste) =>
      liste.map((t) => (t.id === id ? { ...t, faite: !t.faite } : t)));
  }
}

Le sélecteur app-liste-taches permet d’utiliser le composant dans un autre template : <app-liste-taches />. Le template peut aussi être placé dans un fichier séparé (templateUrl), ce que fait le CLI.

Les liaisons du template

Syntaxe Sens Exemple
{{ expr }} interpolation (texte) {{ produit.nom }}
[prop]="expr" propriété, du composant vers le DOM [disabled]="panierVide()"
(evt)="instr" événement, du DOM vers le composant (click)="ajouter()"
[(prop)]="val" liaison bidirectionnelle [(ngModel)]="recherche" (avec FormsModule)

Variantes utiles : [class.active]="estActif", [style.width.px]="largeur", [attr.aria-label]="libelle".

Conditions et listes

Les blocs de contrôle @if, @else, @for, @empty et @switch sont la syntaxe moderne (les anciennes directives *ngIf et *ngFor restent courantes dans le code existant). @for exige une expression track : Angular s’en sert pour savoir quel élément du DOM correspond à quelle donnée quand la liste change. Utilisez un identifiant stable (track tache.id).

Les signaux

Un signal est une valeur réactive : on la lit en l’appelant (taches()), on la modifie avec set(nouvelle) ou update(fn). Angular sait quels templates lisent quels signaux et met à jour uniquement ce qui en dépend. computed() dérive une valeur d’autres signaux, recalculée seulement quand ils changent. Comme avec tout état, on remplace les tableaux et objets par de nouvelles valeurs plutôt que de les modifier sur place.

Communiquer : input() et output()

@Component({
  selector: 'app-carte-produit',
  template: `
    <h3>{{ nom() }}</h3>
    <button (click)="ajouter.emit(id())">Ajouter</button>
  `,
})
export class CarteProduit {
  id = input.required<number>();
  nom = input.required<string>();
  ajouter = output<number>();
}

Le parent écrit : <app-carte-produit [id]="p.id" [nom]="p.nom" (ajouter)="onAjouter($event)" />. Le composant parent doit importer CarteProduit dans son tableau imports. Les décorateurs @Input() et @Output() jouent le même rôle dans le code plus ancien.

Les pipes

Un pipe transforme une valeur pour l’affichage : {{ prix | currency:'EUR' }}, {{ date | date:'dd/MM/yyyy' }}. Un pipe personnalisé est une classe avec une méthode transform ; c’est une fonction pure, idéale pour un test.

À retenir

Classe + décorateur + template ; [ ] vers le DOM, ( ) depuis le DOM ; @for avec track ; signaux lus en les appelant ; input()/output() pour communiquer.

Exercices

Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.

  1. CodeDifficulté 12 indices

    Résumé d'une liste de tâches

    Logique de la computed resume du composant. Déclarez l’interface Tache (id nombre, libelle chaîne, faite booléen) et écrivez resumeTaches(taches: readonly Tache[]): Resume qui renvoie { total, faites, pourcentage }, le pourcentage de tâches faites étant arrondi à l’entier (0 pour une liste vide).

    // TODO : interface Tache et type Resume
    
    function resumeTaches(taches) {
      // TODO
    }
  2. CodeDifficulté 1JavaScript dans le navigateur2 indices

    Pipe tronquer (logique exécutée)

    Écrivez en JavaScript la méthode transform d’un pipe tronquer : tronquer(texte, longueur = 20, suffixe = "…").

    • null ou undefined → chaîne vide ;
    • si le texte (espaces des extrémités retirés) tient dans longueur, il est renvoyé tel quel (sans les espaces des extrémités) ;
    • sinon, on garde les longueur premiers caractères, on retire les espaces finaux, puis on ajoute le suffixe.
    function tronquer(texte, longueur = 20, suffixe = "…") {
      // TODO
    }
  3. TâcheDifficulté 22 indices

    Premier composant Angular

    Créez un projet avec ng new puis un composant ListeTaches (ng g c liste-taches) affiché dans le composant racine.

    1. État : un signal initialisé avec ces tâches :
    id,libelle,faite
    1,Acheter du pain,false
    2,Réviser Angular,true
    3,Appeler le garage,false
    1. Affichez-les avec @for (…; track tache.id) et une case à cocher liée par [checked] et (change).
    2. Ajoutez une computed resume qui utilise votre fonction resumeTaches, affichée sous la forme « 1 / 3 tâches faites (33 %) ».
    3. Ajoutez un champ texte et un bouton « Ajouter » qui ajoute une tâche avec update (nouvel identifiant = plus grand identifiant + 1). Le bouton est désactivé ([disabled]) quand le champ est vide.
    • Les tâches sont dans un signal et lues avec taches() dans le template
    • @for utilise track tache.id
    • Le résumé est une computed, recalculée automatiquement
    • L'ajout remplace le tableau avec update au lieu de faire push
  4. QCMDifficulté 11 indice

    Lire un signal

    Dans le composant, total = signal(0). Comment afficher sa valeur dans le template ?

    • {{ total }}
    • {{ total() }}
    • {{ total.value }}
    • {{ $total }}

Quiz de fin de leçon

  1. Que fait [disabled]="panierVide()" ?

    • Il écoute l'événement disabled
    • Il lie la propriété disabled de l'élément à la valeur de l'expression
    • Il crée une variable disabled
  2. Pourquoi @for exige-t-il une expression track ?

    • Pour trier la liste
    • Pour associer chaque élément du DOM à une donnée et le réutiliser correctement quand la liste change
    • Pour compter les éléments
  3. Comment un composant enfant prévient-il son parent d’une action ?

    • En modifiant son input
    • En émettant un événement avec un output()
    • En appelant directement une méthode du parent

La suite du cours

  1. Services et injection de dépendances18 min
  2. RxJS, observables et client HTTP20 min
  3. Formulaires réactifs et validation19 min
  4. Routage, chargement à la demande et gardes18 min

Angular · Intermédiaire · 1 h 33