-
Notifications
You must be signed in to change notification settings - Fork 0
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 ? 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.
- Introduction
- Anatomie d'un composant
- Mise à jour du composant
- Composition des composants
- Flux de contrôle - @if
- Flux de contrôle - @for
- Liaison de propriété
- Gestion des événements
- Propriétés d'entrée (Input)
- Propriétés de sortie (Output)
- Vues différables (@defer)
- Optimisation des images
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.
// 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>).
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 {}Un composant peut définir son template et ses styles de deux façons :
-
en ligne, via les propriétés
templateetstylesdu décorateur (pratique pour de petits composants), -
dans des fichiers séparés, via
templateUrletstyleUrl(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.
// 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;
}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>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).
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);
}
}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);
}
}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).
// 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 {}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 {}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.
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);
}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);
}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.
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' },
]);
}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' },
]);
}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.).
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);
}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);
}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.
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);
}
}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);
}
}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
// 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>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);
}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).
// 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++;
}
}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);
}
}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
@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 {}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 {}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.
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 {}
widthetheightsont obligatoires (sauf en modefill) afin d'éviter les décalages de mise en page (layout shift).priorityindique qu'il s'agit d'une image importante à charger en premier (ex : image "above the fold").
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 {}
Wiki basé sur le tutoriel officiel Apprendre Angular (angular.dev) — Accueil · Documentation officielle Angular
Démarrage
Le tutoriel (25 leçons)