Skip to content

Composants

Fanny Saez edited this page Jul 18, 2026 · 3 revisions

Composants

Cette page couvre le cœur du tutoriel "Apprendre Angular" : les composants, brique de base de toute application Angular. 12 leçons, chacune avec un cours, un exemple concret, et un exercice.

Pour créer un projet et suivre ces exemples en local, va d'abord voir la page Installation.

C'est quoi, à quoi ça sert, pourquoi ?

  • C'est quoi ? Un composant est une classe TypeScript associée à un template (HTML) et des styles (CSS) : il représente une portion autonome de l'interface. Une application Angular est un arbre de composants imbriqués.
  • À quoi ça sert ? À découper une interface complexe en éléments indépendants, réutilisables et testables (un bouton, une carte produit, une barre de navigation...), chacun responsable d'une petite partie de l'écran.
  • Pourquoi ? Sans cette brique de base, impossible de structurer une application au-delà d'une simple page statique. C'est le concept fondateur sur lequel reposent le routage, les formulaires, les services et les pipes vus dans les pages suivantes.

Sommaire

  1. Introduction
  2. Anatomie d'un composant
  3. Mise à jour du composant
  4. Composition des composants
  5. Flux de contrôle - @if
  6. Flux de contrôle - @for
  7. Liaison de propriété
  8. Gestion des événements
  9. Propriétés d'entrée (Input)
  10. Propriétés de sortie (Output)
  11. Vues différables (@defer)
  12. Optimisation des images

1. Introduction

Cours

Un composant Angular est un morceau réutilisable d'interface utilisateur qui contrôle une portion de l'écran (une "vue"). Une application Angular est un arbre de composants imbriqués : un composant racine, qui contient d'autres composants, qui peuvent eux-mêmes en contenir d'autres.

Chaque composant regroupe trois choses :

  • une classe TypeScript qui contient la logique et les données,
  • un template (HTML) qui décrit ce qui est affiché,
  • des styles (CSS) propres au composant.

Exemple

// hello.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: `<h1>Bonjour le monde !</h1>`,
  styles: `h1 { color: darkviolet; }`,
})
export class Hello {}

Le décorateur @Component transforme une classe TypeScript classique en composant Angular. selector définit le nom de la balise HTML utilisable ailleurs (<app-hello></app-hello>).

Exercice

Crée un composant Bienvenue qui affiche un titre <h2> avec ton prénom et un court paragraphe de présentation. Donne-lui un sélecteur app-bienvenue.

Solution
import { Component } from '@angular/core';

@Component({
  selector: 'app-bienvenue',
  template: `
    <h2>Bienvenue, Fanny !</h2>
    <p>J'apprends Angular pas à pas.</p>
  `,
})
export class Bienvenue {}

2. Anatomie d'un composant

Cours

Un composant peut définir son template et ses styles de deux façons :

  • en ligne, via les propriétés template et styles du décorateur (pratique pour de petits composants),
  • dans des fichiers séparés, via templateUrl et styleUrl (plus lisible pour des composants plus complexes).

C'est ce que fait ng generate component par défaut : il crée un fichier .ts, un .html et un .css.

Exemple

// profil.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-profil',
  templateUrl: './profil.html',
  styleUrl: './profil.css',
})
export class Profil {
  nom = 'Fanny';
  role = 'Développeuse Angular en apprentissage';
}
<!-- profil.html -->
<h2>{{ nom }}</h2>
<p>{{ role }}</p>
/* profil.css */
h2 {
  font-family: sans-serif;
}

Exercice

Transforme ton composant Bienvenue de l'exercice précédent pour qu'il utilise templateUrl et styleUrl au lieu de template/styles en ligne. Ajoute une propriété prenom utilisée dans le template avec l'interpolation {{ }}.

Solution
// bienvenue.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-bienvenue',
  templateUrl: './bienvenue.html',
  styleUrl: './bienvenue.css',
})
export class Bienvenue {
  prenom = 'Fanny';
}
<!-- bienvenue.html -->
<h2>Bienvenue, {{ prenom }} !</h2>
<p>J'apprends Angular pas à pas.</p>

3. Mise à jour du composant

Cours

Les propriétés d'une classe de composant peuvent être des signaux (signal()), l'approche moderne et recommandée pour gérer un état réactif en Angular. Un signal est un conteneur de valeur qui notifie automatiquement Angular quand elle change, afin que le template se mette à jour tout seul.

  • On lit un signal en l'appelant : count().
  • On le modifie avec .set(nouvelleValeur) ou .update(valeurActuelle => nouvelleValeur).

Exemple

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

@Component({
  selector: 'app-compteur',
  template: `
    <p>Compteur : {{ count() }}</p>
    <button (click)="incrementer()">+1</button>
  `,
})
export class Compteur {
  count = signal(0);

  incrementer() {
    this.count.update(valeur => valeur + 1);
  }
}

Exercice

Crée un composant Panier avec un signal articles initialisé à 0, un bouton "Ajouter un article" qui incrémente le signal, et un bouton "Vider le panier" qui le remet à 0.

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

@Component({
  selector: 'app-panier',
  template: `
    <p>Articles dans le panier : {{ articles() }}</p>
    <button (click)="ajouter()">Ajouter un article</button>
    <button (click)="vider()">Vider le panier</button>
  `,
})
export class Panier {
  articles = signal(0);

  ajouter() {
    this.articles.update(n => n + 1);
  }

  vider() {
    this.articles.set(0);
  }
}

4. Composition des composants

Cours

Un composant peut en utiliser un autre dans son template : c'est la composition. Il faut alors importer le composant enfant dans le tableau imports du composant parent (les composants standalone gèrent leurs dépendances eux-mêmes, sans NgModule).

Exemple

// enfant.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-enfant',
  template: `<p>Je suis le composant enfant.</p>`,
})
export class Enfant {}
// parent.ts
import { Component } from '@angular/core';
import { Enfant } from './enfant';

@Component({
  selector: 'app-parent',
  imports: [Enfant],
  template: `
    <h2>Composant parent</h2>
    <app-enfant></app-enfant>
  `,
})
export class Parent {}

Exercice

Crée un composant CarteProduit qui affiche le nom d'un produit, puis intègre-le trois fois dans un composant Catalogue.

Solution
// carte-produit.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-carte-produit',
  template: `<div class="carte"><p>Produit</p></div>`,
})
export class CarteProduit {}
// catalogue.ts
import { Component } from '@angular/core';
import { CarteProduit } from './carte-produit';

@Component({
  selector: 'app-catalogue',
  imports: [CarteProduit],
  template: `
    <h2>Catalogue</h2>
    <app-carte-produit></app-carte-produit>
    <app-carte-produit></app-carte-produit>
    <app-carte-produit></app-carte-produit>
  `,
})
export class Catalogue {}

5. Flux de contrôle - @if

Cours

Le bloc @if (nouvelle syntaxe de control flow, introduite en Angular 17, qui remplace *ngIf) permet d'afficher conditionnellement une portion de template. On peut y ajouter @else if et @else.

Exemple

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

@Component({
  selector: 'app-statut',
  template: `
    @if (connecte()) {
      <p>Bienvenue !</p>
    } @else {
      <p>Merci de te connecter.</p>
    }
  `,
})
export class Statut {
  connecte = signal(false);
}

Exercice

Crée un composant Meteo avec un signal temperature (nombre). Affiche "Il fait froid" si la température est inférieure à 10, "Il fait doux" si elle est entre 10 et 25, et "Il fait chaud" au-delà, en utilisant @if / @else if / @else.

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

@Component({
  selector: 'app-meteo',
  template: `
    @if (temperature() < 10) {
      <p>Il fait froid</p>
    } @else if (temperature() <= 25) {
      <p>Il fait doux</p>
    } @else {
      <p>Il fait chaud</p>
    }
  `,
})
export class Meteo {
  temperature = signal(18);
}

6. Flux de contrôle - @for

Cours

Le bloc @for (qui remplace *ngFor) permet d'itérer sur une collection. Il exige un identifiant unique via track (une clé stable, essentielle pour les performances car elle permet à Angular de savoir quels éléments ont réellement changé). On peut fournir un bloc @empty affiché quand la collection est vide.

Exemple

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

@Component({
  selector: 'app-liste-taches',
  template: `
    <ul>
      @for (tache of taches(); track tache.id) {
        <li>{{ tache.titre }}</li>
      } @empty {
        <li>Aucune tâche pour le moment.</li>
      }
    </ul>
  `,
})
export class ListeTaches {
  taches = signal([
    { id: 1, titre: 'Apprendre Angular' },
    { id: 2, titre: 'Faire les exercices' },
  ]);
}

Exercice

Crée un composant ListeCourses avec un signal contenant un tableau d'objets { id, nom }. Affiche-les dans une liste <ul> avec @for et un @empty qui affiche "Panier vide".

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

@Component({
  selector: 'app-liste-courses',
  template: `
    <ul>
      @for (article of articles(); track article.id) {
        <li>{{ article.nom }}</li>
      } @empty {
        <li>Panier vide</li>
      }
    </ul>
  `,
})
export class ListeCourses {
  articles = signal([
    { id: 1, nom: 'Pommes' },
    { id: 2, nom: 'Pain' },
    { id: 3, nom: 'Lait' },
  ]);
}

7. Liaison de propriété

Cours

La liaison de propriété (property binding), avec la syntaxe [propriété]="expression", permet de lier dynamiquement un attribut ou une propriété DOM à une valeur de la classe du composant. Contrairement à l'interpolation {{ }} (qui insère du texte), le property binding cible directement une propriété de l'élément (comme src, disabled, class, etc.).

Exemple

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

@Component({
  selector: 'app-image-profil',
  template: `
    <img [src]="urlImage()" [alt]="description()">
    <button [disabled]="chargementEnCours()">Valider</button>
  `,
})
export class ImageProfil {
  urlImage = signal('/assets/avatar.png');
  description = signal("Photo de profil");
  chargementEnCours = signal(true);
}

Exercice

Crée un composant BoutonAlerte avec un signal booléen estActif. Utilise le property binding pour désactiver un <button> quand estActif() vaut false, et pour lui appliquer la classe CSS "actif" via [class.actif] quand il vaut true.

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

@Component({
  selector: 'app-bouton-alerte',
  template: `
    <button [disabled]="!estActif()" [class.actif]="estActif()">
      Envoyer l'alerte
    </button>
  `,
})
export class BoutonAlerte {
  estActif = signal(true);
}

8. Gestion des événements

Cours

La syntaxe (événement)="expression" permet d'écouter un événement DOM (clic, saisie, survol...) et d'exécuter du code en réponse. L'objet événement natif est accessible via $event.

Exemple

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

@Component({
  selector: 'app-recherche',
  template: `
    <input (input)="onSaisie($event)" placeholder="Rechercher...">
    <p>Résultat : {{ terme() }}</p>
  `,
})
export class Recherche {
  terme = signal('');

  onSaisie(event: Event) {
    const valeur = (event.target as HTMLInputElement).value;
    this.terme.set(valeur);
  }
}

Exercice

Crée un composant Interrupteur avec un signal booléen allume (initialisé à false). Ajoute un bouton qui, au clic, inverse la valeur du signal, et affiche "Allumé" ou "Éteint" selon l'état.

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

@Component({
  selector: 'app-interrupteur',
  template: `
    <button (click)="basculer()">
      {{ allume() ? 'Allumé' : 'Éteint' }}
    </button>
  `,
})
export class Interrupteur {
  allume = signal(false);

  basculer() {
    this.allume.update(v => !v);
  }
}

9. Propriétés d'entrée (Input)

Cours

Une propriété d'entrée (input) permet à un composant parent de transmettre des données à un composant enfant. On la déclare avec la fonction input() :

  • input<T>() : entrée optionnelle
  • input.required<T>() : entrée obligatoire, doit être fournie par le parent

Exemple

// carte-produit.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-carte-produit',
  template: `
    <h3>{{ nom() }}</h3>
    <p>{{ prix() }} €</p>
  `,
})
export class CarteProduit {
  nom = input.required<string>();
  prix = input(0);
}
<!-- utilisation dans le parent -->
<app-carte-produit [nom]="'Clavier mécanique'" [prix]="79"></app-carte-produit>

Exercice

Modifie le composant CarteProduit de la leçon 4 pour qu'il accepte deux entrées : nom (obligatoire, string) et enStock (optionnelle, boolean, par défaut true). Affiche "Rupture de stock" si enStock() est false.

Solution
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-carte-produit',
  template: `
    <h3>{{ nom() }}</h3>
    @if (!enStock()) {
      <p>Rupture de stock</p>
    }
  `,
})
export class CarteProduit {
  nom = input.required<string>();
  enStock = input(true);
}

10. Propriétés de sortie (Output)

Cours

Une propriété de sortie (output) permet à un composant enfant d'émettre des événements vers son parent. On la déclare avec la fonction output(), puis on émet une valeur avec .emit(valeur).

Exemple

// bouton-like.ts
import { Component, output } from '@angular/core';

@Component({
  selector: 'app-bouton-like',
  template: `<button (click)="liker()">J'aime</button>`,
})
export class BoutonLike {
  like = output<void>();

  liker() {
    this.like.emit();
  }
}
// parent.ts
import { Component } from '@angular/core';
import { BoutonLike } from './bouton-like';

@Component({
  selector: 'app-parent',
  imports: [BoutonLike],
  template: `
    <app-bouton-like (like)="onLike()"></app-bouton-like>
    <p>Nombre de likes : {{ nbLikes }}</p>
  `,
})
export class Parent {
  nbLikes = 0;

  onLike() {
    this.nbLikes++;
  }
}

Exercice

Crée un composant enfant ChampAvis avec un <input> texte et un bouton "Envoyer". Au clic, il doit émettre un output avisEnvoye (de type string) contenant le texte saisi. Dans un composant parent, écoute cet événement et affiche l'avis reçu.

Solution
// champ-avis.ts
import { Component, output, signal } from '@angular/core';

@Component({
  selector: 'app-champ-avis',
  template: `
    <input [value]="texte()" (input)="onSaisie($event)">
    <button (click)="envoyer()">Envoyer</button>
  `,
})
export class ChampAvis {
  avisEnvoye = output<string>();
  texte = signal('');

  onSaisie(event: Event) {
    this.texte.set((event.target as HTMLInputElement).value);
  }

  envoyer() {
    this.avisEnvoye.emit(this.texte());
  }
}
// parent.ts
import { Component, signal } from '@angular/core';
import { ChampAvis } from './champ-avis';

@Component({
  selector: 'app-parent',
  imports: [ChampAvis],
  template: `
    <app-champ-avis (avisEnvoye)="onAvis($event)"></app-champ-avis>
    <p>Dernier avis : {{ dernierAvis() }}</p>
  `,
})
export class Parent {
  dernierAvis = signal('');

  onAvis(avis: string) {
    this.dernierAvis.set(avis);
  }
}

11. Vues différables (@defer)

Cours

Le bloc @defer permet de différer le chargement d'une partie du template (et du code JavaScript associé) jusqu'à ce qu'une condition soit remplie : au clic, au survol, quand l'élément devient visible (on viewport), après un délai, etc. Cela réduit la taille du bundle initial et améliore les performances de chargement.

Blocs complémentaires possibles :

  • @placeholder : contenu affiché avant le déclenchement
  • @loading : contenu affiché pendant le chargement
  • @error : contenu affiché en cas d'échec

Exemple

@Component({
  selector: 'app-page-produit',
  imports: [Avis],
  template: `
    <h2>Détail du produit</h2>

    @defer (on viewport) {
      <app-avis></app-avis>
    } @placeholder {
      <p>Les avis s'afficheront ici.</p>
    } @loading {
      <p>Chargement des avis...</p>
    }
  `,
})
export class PageProduit {}

Exercice

Crée un bloc @defer qui charge un composant Commentaires uniquement au clic sur un bouton "Afficher les commentaires" (déclencheur on interaction), avec un @placeholder affichant le bouton et un @loading affichant "Chargement...".

Solution
@Component({
  selector: 'app-article',
  imports: [Commentaires],
  template: `
    <h2>Mon article</h2>

    @defer (on interaction) {
      <app-commentaires></app-commentaires>
    } @placeholder {
      <button>Afficher les commentaires</button>
    } @loading {
      <p>Chargement...</p>
    }
  `,
})
export class Article {}

12. Optimisation des images

Cours

La directive NgOptimizedImage (à importer depuis @angular/common) remplace l'attribut src par ngSrc et applique automatiquement de bonnes pratiques de performance : lazy loading, srcset responsive, avertissements en cas d'image mal dimensionnée, préchargement des images prioritaires via l'attribut priority.

Exemple

import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-bannière',
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="/assets/banniere.jpg" width="1200" height="400" priority>
  `,
})
export class Banniere {}

width et height sont obligatoires (sauf en mode fill) afin d'éviter les décalages de mise en page (layout shift). priority indique qu'il s'agit d'une image importante à charger en premier (ex : image "above the fold").

Exercice

Convertis une balise <img src="/assets/logo.png"> en utilisant NgOptimizedImage, avec une largeur de 200, une hauteur de 80, et marque-la comme prioritaire.

Solution
import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-logo',
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="/assets/logo.png" width="200" height="80" priority>
  `,
})
export class Logo {}

Précédent : Installation     Suivant : Routage

Apprendre Angular

Accueil

Démarrage

Le tutoriel (25 leçons)

Clone this wiki locally