Prérequis : bases de HTML, JavaScript et TypeScript. Travaillez dans une application Angular 20 autonome créée avec Angular CLI 20, avec Node.js compatible (20.19+, 22.12+ ou 24.x). Chaque solution fournit le fichier src/main.ts complet ; remplacez-le pour essayer un exercice, sans mélanger les exemples. Les imports communs sont volontairement conservés pour faciliter les essais. Le fichier index.html du projet doit contenir app-root. Les formulaires sont des démonstrations locales, sans envoi à un serveur.
Méthode : répondez à la question de cours, réalisez l’exercice, puis ouvrez le corrigé et vérifiez les cas proposés. Comptez 10 à 25 minutes par exercice et 45 à 60 minutes pour le mini-projet.
Préparer les essais
npx --package @angular/cli@20 ng new atelier-nkilitech --standalone --routing=false --ssr=false --skip-tests --defaults
cd atelier-nkilitech
# Remplacer src/main.ts par une correction, puis :
npx ng serve
20 exercices · 20 questions de cours · 20 corrigés
Composants et templates
1
Comprendre un composant
Question de cours. Quels rôles jouent la classe, le template et le sélecteur ?
À réaliser. Créer un composant autonome présentant NKILItech.
Voir le corrigé 1Masquer le corrigé 1
Réponse et raisonnement. La classe contient les données et comportements ; le template décrit la vue ; le sélecteur indique le nom HTML du composant. bootstrapApplication démarre ce composant racine.
Programme complet
import { Component, Injectable, Input, Output, EventEmitter, inject, signal, computed } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
styles: [`.complet { font-weight: bold; border-left: 4px solid #c0392b; padding-left: 1rem; }`],
template: `<h1>Bienvenue chez {{ centre }}</h1>`
})
class AppComponent {
centre = "NKILItech";
}
bootstrapApplication(AppComponent).catch(error => console.error(error));
Résultat attendu et vérification.
Le titre affiche Bienvenue chez NKILItech.
2
Interpolation et propriété
Question de cours. Quelle différence entre {{ expression }} et [disabled] ?
À réaliser. Afficher une formation et désactiver le bouton lorsque les places valent zéro.
Voir le corrigé 2Masquer le corrigé 2
Réponse et raisonnement. L’interpolation produit du texte. La liaison de propriété transmet une valeur typée à la propriété du bouton : false le réactive. Un attribut HTML disabled="false" resterait au contraire présent et donc désactivant.
Le bouton est désactivé à zéro ; remplacer places par 2 le réactive.
3
Réagir à un clic
Question de cours. À quoi sert une liaison (click) ?
À réaliser. Incrémenter un compteur de demandes à chaque clic.
Voir le corrigé 3Masquer le corrigé 3
Réponse et raisonnement. La liaison appelle une méthode de la classe lors de l’événement. L’état initial appartient à la classe ; aucun accès manuel au DOM n’est nécessaire.
Question de cours. Quand choisir @if plutôt que masquer seulement avec CSS ?
À réaliser. Afficher Disponible pour trois places, puis Complet pour zéro.
Voir le corrigé 4Masquer le corrigé 4
Réponse et raisonnement. @if crée la branche correspondante dans la vue. La branche inactive n’est pas seulement rendue invisible. La condition doit utiliser une comparaison explicite de la capacité.
Avec 3, Disponible apparaît ; avec 0, seul Complet apparaît.
5
Boucler sur un catalogue
Question de cours. Pourquoi une clé track stable est-elle utile ?
À réaliser. Afficher HTML et Kotlin avec leurs identifiants 1 et 2 ; traiter aussi un catalogue vide.
Voir le corrigé 5Masquer le corrigé 5
Réponse et raisonnement. track relie chaque objet à son élément de vue. Une clé métier unique reste stable si la liste est réordonnée. @empty fournit le message quand aucune formation n’existe.
Deux lignes sont visibles ; avec catalogue = [], le message Aucun atelier les remplace.
État et interactions
6
Lire et modifier un signal
Question de cours. Pourquoi lit-on un signal avec des parenthèses ?
À réaliser. Créer un nombre de places réactif et réserver sans passer sous zéro.
Voir le corrigé 6Masquer le corrigé 6
Réponse et raisonnement. Le signal est un accesseur réactif : places() lit sa valeur. update calcule la nouvelle valeur depuis la précédente. La méthode protège aussi la règle métier, même si le bouton est désactivé.
Deux clics donnent zéro ; le bouton devient désactivé et le nombre ne devient pas négatif.
7
Calculer un état dérivé
Question de cours. Pourquoi computed évite-t-il une seconde variable à synchroniser ?
À réaliser. Calculer automatiquement le total pour une quantité de places à 15 000 FCFA.
Voir le corrigé 7Masquer le corrigé 7
Réponse et raisonnement. Le total dépend de quantite. computed recalcule la valeur dérivée quand la dépendance change ; aucun événement séparé ne doit remettre manuellement le total à jour.
Au départ : 15000 FCFA ; après un clic : 30000 FCFA.
8
Saisir avec une référence de template
Question de cours. Quel objet désigne #recherche sur un input ?
À réaliser. Filtrer HTML, Kotlin et Angular selon une recherche insensible à la casse.
Voir le corrigé 8Masquer le corrigé 8
Réponse et raisonnement. La référence désigne ici l’élément input. Sa valeur est transmise au signal ; computed applique le filtre. trim rend une recherche composée d’espaces équivalente à une recherche vide.
KO affiche Kotlin ; xyz affiche Aucun résultat ; une saisie vide restaure les trois formations.
9
Appliquer une classe conditionnelle
Question de cours. Pourquoi préférer [class.complet] à une modification manuelle de classList ?
À réaliser. Mettre en évidence l’état complet et permettre de basculer la disponibilité.
Voir le corrigé 9Masquer le corrigé 9
Réponse et raisonnement. La classe visuelle dépend directement de l’état. Le rendu reste cohérent après chaque changement. Le libellé Complet est aussi présent : la couleur seule ne doit pas porter l’information.
Le clic change le texte et ajoute ou retire la classe complet ; inspecter l’élément pour le vérifier.
10
Modifier une liste réactive
Question de cours. Pourquoi créer un nouveau tableau pour mettre à jour un signal ?
À réaliser. Ajouter une formation Rust à une liste initiale HTML, sans doublon.
Voir le corrigé 10Masquer le corrigé 10
Réponse et raisonnement. Le nouveau tableau possède une référence différente, ce qui rend le changement observable avec la comparaison par défaut. Le contrôle includes rend un second clic sans effet.
Programme complet
import { Component, Injectable, Input, Output, EventEmitter, inject, signal, computed } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
styles: [`.complet { font-weight: bold; border-left: 4px solid #c0392b; padding-left: 1rem; }`],
template: `<h1>Catalogue réactif</h1><ul>@for (nom of cours(); track nom) { <li>{{ nom }}</li> }</ul><button type="button" (click)="ajouter()">Ajouter Rust</button>`
})
class AppComponent {
cours = signal(["HTML"]);
ajouter() { this.cours.update(liste => liste.includes("Rust") ? liste : [...liste, "Rust"]); }
}
bootstrapApplication(AppComponent).catch(error => console.error(error));
Résultat attendu et vérification.
Le premier clic ajoute Rust ; dix clics ne créent toujours que deux lignes.
Formulaires et composants
11
Valider un champ réactif
Question de cours. Pourquoi le contrôle de formulaire porte-t-il sa propre validité ?
À réaliser. Créer un courriel obligatoire valide avant d’activer Confirmer.
Voir le corrigé 11Masquer le corrigé 11
Réponse et raisonnement. FormControl rassemble valeur et règles de validation. nonNullable évite que reset produise null. Validators.email contrôle une forme syntaxique, sans prouver l’existence de la boîte.
Vide ou abc désactive le bouton ; eleve@example.org l’active. Aucune donnée n’est envoyée.
12
Regrouper un formulaire
Question de cours. Quelle différence entre un FormControl et un FormGroup ?
À réaliser. Recueillir un nom obligatoire et entre 1 et 5 places entières, puis afficher la demande.
Voir le corrigé 12Masquer le corrigé 12
Réponse et raisonnement. Le groupe réunit plusieurs contrôles et agrège leur validité. Les validateurs min/max limitent la plage ; pattern refuse une quantité décimale. Le gestionnaire vérifie également la validité.
Nom vide, espaces seuls, 0, 6 et 1,5 sont refusés ; Amina avec 2 donne Amina : 2 places.
13
Partager une logique avec un service
Question de cours. Que signifie providedIn: "root" dans cet exemple ?
À réaliser. Injecter un service qui fournit deux formations sans recopier le catalogue dans le composant.
Voir le corrigé 13Masquer le corrigé 13
Réponse et raisonnement. Le service est fourni à la racine de l’application. inject demande l’instance au système d’injection ; le composant ne la construit pas avec new. Le service de cet exercice est défini avant le composant dans le fichier complet.
Programme complet
import { Component, Injectable, Input, Output, EventEmitter, inject, signal, computed } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
@Injectable({providedIn: "root"})
class CatalogueService { readonly catalogue = ["HTML", "Angular"]; }
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
styles: [`.complet { font-weight: bold; border-left: 4px solid #c0392b; padding-left: 1rem; }`],
template: `<h1>Catalogue du service</h1><ul>@for (nom of service.catalogue; track nom) { <li>{{ nom }}</li> }</ul>`
})
class AppComponent {
service = inject(CatalogueService);
}
bootstrapApplication(AppComponent).catch(error => console.error(error));
Résultat attendu et vérification.
HTML et Angular apparaissent à partir des données du service.
14
Passer une donnée à un enfant
Question de cours. Dans quel sens circule une entrée @Input ?
À réaliser. Créer une carte enfant qui reçoit le nom Kotlin depuis son parent.
Voir le corrigé 14Masquer le corrigé 14
Réponse et raisonnement. Le parent fournit la donnée via une liaison de propriété ; l’enfant l’affiche. Le composant enfant autonome doit être importé dans les imports du parent pour que son sélecteur soit reconnu.
Le titre de la carte enfant affiche Kotlin ; changer selection change la carte.
15
Émettre un événement enfant
Question de cours. Pourquoi un enfant émet-il une intention au lieu de modifier directement le parent ?
À réaliser. Faire émettre Kotlin par une carte quand on clique Choisir ; afficher le choix dans le parent.
Voir le corrigé 15Masquer le corrigé 15
Réponse et raisonnement. EventEmitter expose un événement typé. Le parent reçoit $event et décide de l’effet sur son état ; l’enfant reste réutilisable et ne connaît pas la structure du parent.
Le clic sur Choisir Kotlin remplace aucun par Kotlin.
Robustesse et mini-projet
16
Afficher les états d’un chargement
Question de cours. Pourquoi distinguer chargement, réussite et échec ?
À réaliser. Simuler une demande locale asynchrone, avec un bouton réussite et un bouton échec.
Voir le corrigé 16Masquer le corrigé 16
Réponse et raisonnement. Le même emplacement affiche les états exclusifs. Le rejet est intercepté afin d’éviter une promesse non traitée. Les boutons bloqués durant l’attente empêchent des demandes concurrentes dans cette démonstration.
Charger aboutit à Catalogue disponible ; l’autre bouton affiche Échec : réessayez après l’attente.
17
Afficher du texte non fiable
Question de cours. Pourquoi ne pas contourner la protection pour afficher une saisie ordinaire ?
À réaliser. Afficher littéralement une chaîne contenant une balise HTML.
Voir le corrigé 17Masquer le corrigé 17
Réponse et raisonnement. L’interpolation traite la valeur comme du texte : la balise ne devient pas un élément de la page. Il n’est pas nécessaire d’utiliser innerHTML ni de déclarer la chaîne fiable pour un simple message.
Les chevrons et strong restent visibles ; aucun élément strong n’est créé par la chaîne.
18
Prévoir une liste vide après suppression
Question de cours. Pourquoi supprimer par identifiant plutôt que par position affichée ?
À réaliser. Supprimer une demande ciblée et montrer Aucun dossier lorsque tout est retiré.
Voir le corrigé 18Masquer le corrigé 18
Réponse et raisonnement. L’identifiant conserve son sens après tri ou filtrage. filter crée un nouveau tableau en excluant le dossier visé ; @empty gère l’absence de données sans accès à un élément inexistant.
Retirer Amina conserve Boris ; retirer Boris affiche Aucun dossier.
19
Tester une fonction métier
Question de cours. Pourquoi isoler un calcul pur de l’interface ?
À réaliser. Calculer un tarif entier et tester trois quantités ainsi qu’un cas invalide, sans dépendre d’un clic.
Voir le corrigé 19Masquer le corrigé 19
Réponse et raisonnement. Une fonction pure est testable indépendamment du composant. Les exceptions signalent un contrat invalide ; les tests doivent réellement échouer quand une attente est fausse, plutôt que seulement écrire un message.
Programme complet
import { Component, Injectable, Input, Output, EventEmitter, inject, signal, computed } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
function tarif(n: number): number {
if (!Number.isInteger(n) || n < 0) throw new Error("Quantité invalide");
return n * 15000;
}
function verifierTarif(): string {
for (const [n, attendu] of [[0, 0], [1, 15000], [3, 45000]]) {
if (tarif(n) !== attendu) throw new Error("Tarif incorrect");
}
let refuse = false;
try { tarif(-1); } catch { refuse = true; }
if (!refuse) throw new Error("La quantité négative doit être refusée");
return "4 vérifications réussies";
}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
styles: [`.complet { font-weight: bold; border-left: 4px solid #c0392b; padding-left: 1rem; }`],
template: `<h1>Vérification du tarif</h1><p>{{ bilan }}</p>`
})
class AppComponent {
bilan = verifierTarif();
}
bootstrapApplication(AppComponent).catch(error => console.error(error));
Résultat attendu et vérification.
Le composant affiche 4 vérifications réussies. Modifier une attente erronée provoque une erreur explicite.
20
Mini-projet : demandes NKILItech
Question de cours. Comment articuler formulaire, état réactif et total dérivé ?
À réaliser. Gérer un maximum de cinq places : saisir un nom, ajouter une demande, afficher le total à 15 000 FCFA par place et pouvoir retirer une demande.
Voir le corrigé 20Masquer le corrigé 20
Réponse et raisonnement. Le formulaire valide le nom, le signal contient les dossiers, computed calcule places restantes et recette. Un compteur produit des identifiants stables. Le gestionnaire contrôle la capacité même si le bouton est désactivé. Tout reste en mémoire et disparaît au rechargement.