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

Le TP 2 : Compteur interactif avec Angular

Ce travail pratique de 6 heures réinvestit tous les chapitres du cours depuis TypeScript. On y construit l’application « Compteurs interactifs » en dix parties guidées : un modèle TypeScript typé (un...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Ce travail pratique de 6 heures réinvestit tous les chapitres du cours depuis TypeScript. On y construit l’application « Compteurs interactifs » en dix parties guidées : un modèle TypeScript typé (unions, interfaces, fonctions) ; un compteur numérique géré par des signals, avec ses limites, ses couleurs et sa jauge (liaisons de propriété, de classe, de style et d’attribut) ; des réglages en two-way binding avec [(ngModel)] et linkedSignal ; un compteur ASCII avec computed, @switch, des pipes et une variable de référence ; des raccourcis clavier globaux avec host, et l’annulation ; un historique avec input(), output(), @for, @let et un enregistrement par effect ; un composant réutilisable avec model(), et des variantes individuelles.

🎬 La leçon en vidéo : l'activité 2 corrigée pas à pas (20 min 01)

La vidéo ne s'affiche pas ? La regarder sur YouTube.

Durée Niveau Outils Livrable
6 heures (10 parties) Débutant, après les 11 premiers chapitres Node.js 22.18+, Angular CLI 22, VS Code, Git Dépôt Git avec un commit par partie et un README.md
  1. Objectifs

    • Réaliser une application Angular complète, en partant d'un projet vide, en réinvestissant toutes les notions vues jusqu'ici
    • Concevoir un modèle TypeScript typé (unions, interfaces, fonctions) et l'utiliser dans plusieurs composants
    • Gérer l'état avec les signals (signal, computed, linkedSignal, effect)
    • Utiliser toutes les liaisons: interpolation, propriété, classe, style, attribut, événement et two-way ([(ngModel)], model())
    • Gérer le clavier et les événements globaux, et faire communiquer des composants avec input() et output()
    • Exploiter les variables de template (#ref, @let, $index) et le contrôle de flux (@if, @for, @switch)
    • Travailler par étapes vérifiables, avec un commit Git à chaque point de contrôle
  1. Prérequis

  1. Présentation du projet

    • Vous allez développer l'application Compteurs interactifs, composée de quatre zones :
      • Compteur numérique: une valeur entre un minimum et un maximum, modifiée par un pas réglable, en vert si elle est positive et en rouge si elle est négative, avec une jauge, des raccourcis clavier et une fonction d'annulation ;
      • Compteur ASCII: un caractère entre A (65) et z (122), coloré selon sa nature (majuscule, minuscule, symbole), avec son code en hexadécimal et en binaire ;
      • Historique: la liste des dernières actions des deux compteurs, des statistiques, et un enregistrement dans le navigateur ;
      • Mes compteurs du jour: une liste de petits compteurs à objectif (verres d'eau, pauses…), construite à partir d'un composant réutilisable.

Image de référence : l'application Compteurs interactifs terminée

Architecture de l'application : six composants et un modèle TypeScript

  1. Organisation du TP

    • Le TP est découpé en dix parties. Chacune indique sa durée, les notions mobilisées, les consignes, une aide, un point de contrôle à vérifier avant de continuer, et des questions.
    • Répondez aux questions dans un fichier README.md, à la racine du projet, au fur et à mesure.
    • À la fin de chaque partie, faites un commit: git commit -m "Partie N : …". L'historique Git sert de preuve de votre progression.
    • Les aides donnent des squelettes de code, jamais la solution complète : c'est à vous de compléter.

Déroulement du TP sur 6 heures

Notions du cours réinvesties dans chaque partie du TP

  1. Partie 0 : mise en place du projet

⏱ 20 min

Notions mobilisées : La syntaxe de base

  • Créez le projet, puis lancez le serveur de développement :
npx @angular/cli@latest new tp2-compteur --defaults --ssr=false --style=css
cd tp2-compteur
npx ng serve
  • Générez les six composants et créez le dossier du modèle :
npx ng generate component compteur-numerique
npx ng generate component compteur-ascii
npx ng generate component historique
npx ng generate component mes-compteurs
npx ng generate component mini-compteur
mkdir src/app/modeles
  • Videz app.html, puis placez-y un titre <h1>Compteurs interactifs</h1> et les balises <app-compteur-numerique />, <app-compteur-ascii />, <app-historique /> et <app-mes-compteurs />. N'oubliez pas le tableau imports de App.
  • Dans styles.css, définissez les couleurs de l'application en variables CSS (--bleu, --vert, --rouge, --gris) et une classe .carte-tp (fond blanc, coins arrondis, ombre légère). Dans app.css, disposez les quatre zones sur deux colonnes avec une grille CSS.
src/app/
├── app.ts / app.html / app.css
├── modeles/compteur.ts                 ← partie 1
├── compteur-numerique/                 ← parties 2, 3, 4 et 6
├── compteur-ascii/                     ← partie 5
├── historique/                         ← partie 7
├── mes-compteurs/                      ← partie 8
└── mini-compteur/                      ← partie 8

✅ Point de contrôle

  • ng serve fonctionne et la page affiche le titre et les quatre messages « … works! » sur deux colonnes.
  • La console du navigateur (F12) ne contient aucune erreur.

❓ Questions (à répondre dans le README.md)

  1. Quels trois fichiers la commande ng generate component a-t-elle créés pour chaque composant ? Quel est le sélecteur de MiniCompteur?
  2. Que se passe-t-il si l'on oublie d'ajouter un composant au tableau imports de App? Notez le code de l'erreur.
git add .
git commit -m "Partie 0 : création du projet et des composants"
  1. Partie 1 : le modèle TypeScript

⏱ 30 min

Notions mobilisées : Présentation de TypeScript · Les types de variables · Le typage fort

  • Toutes les données de l'application sont décrites dans un seul fichier, src/app/modeles/compteur.ts. Il ne contient aucun code Angular : uniquement des types et des fonctions.
    • un type NomCompteur qui n'accepte que 'numerique' ou 'ascii';
    • un type Operation: 'plus', 'moins', 'reinitialiser', 'annuler' ou 'saisie';
    • un type TypeCaractere: 'majuscule', 'minuscule' ou 'autre';
    • une interface Action: id (chaîne, en lecture seule), compteur, operation, avant, apres (nombres) et heure (Date) ;
    • une interface Reglages: pas, min et max;
    • deux constantes exportées ASCII_MIN = 65 et ASCII_MAX = 122;
    • trois fonctions typées : borner(valeur, min, max) qui ramène une valeur dans l'intervalle, typeCaractere(code), et creerAction(compteur, operation, avant, apres) qui renvoie une Action datée avec un identifiant unique (crypto.randomUUID()).
  • Aide: complétez ce squelette.
export type NomCompteur = /* à compléter */;
export type Operation = /* à compléter */;
export type TypeCaractere = /* à compléter */;

export interface Action {
  readonly id: string;
  /* à compléter */
}

export function borner(valeur: number, min: number, max: number): number {
  // indice : Math.min et Math.max
}

export function typeCaractere(code: number): TypeCaractere {
  // A–Z : 65 à 90 ; a–z : 97 à 122
}
  • Vérifiez vos fonctions avec quelques console.log provisoires dans le constructeur de App:
console.log(borner(15, -10, 10));   // 10
console.log(borner(-3, -10, 10));   // -3
console.log(typeCaractere(77), typeCaractere(100), typeCaractere(91));   // majuscule minuscule autre
console.log(creerAction('numerique', 'plus', 0, 1));   // { id: '…', compteur: 'numerique', …, heure: Date }

✅ Point de contrôle

  • Les quatre lignes de test affichent exactement les résultats attendus, puis vous les supprimez.
  • creerAction('numerique', 'ajouter', 0, 1) est refusé par l'éditeur : 'ajouter' n'est pas une Operation.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi utiliser des unions de chaînes ('plus' | 'moins' | …) plutôt que le type string?
  2. Pourquoi id est-il readonly?
  3. Pourquoi un compteur (1, 2, 3…) ne suffirait-il pas comme identifiant, une fois l'historique enregistré dans le navigateur (partie 7) ?
git add .
git commit -m "Partie 1 : modèle TypeScript"
  1. Partie 2 : le compteur numérique, signals et événements

⏱ 45 min

Notions mobilisées : La syntaxe de base · Le data binding · Les signals · La gestion des événements · Les décorateurs

  • Dans CompteurNumerique:
    • déclarez un signal valeur (0) et un signal pas (1) ;
    • affichez la valeur entre deux boutons − 1 et + 1 (le texte des boutons utilise pas()) ; les boutons appellent moins() et plus();
    • ajoutez un bouton Réinitialiser;
    • toutes les modifications passent par une seule méthode privée changer(operation, cible);
    • déclarez une sortie action = output<Action>(): changer() l'émet avec creerAction(…).
  • Dans App, déclarez un signal historique (tableau d'Action), écoutez la sortie avec (action)="noter($event)" et affichez dans l'en-tête le nombre d'actions enregistrées.
protected changer(operation: Operation, cible: number): void {
  const avant = this.valeur();
  // 1. modifier le signal   2. émettre l'action
}

Point de contrôle de la partie 2 : la valeur 3

✅ Point de contrôle

  • Trois clics sur + 1 affichent 3 ; l'en-tête affiche « 3 action(s) enregistrée(s) ».
  • Réinitialiser remet la valeur à 0 et ajoute une action.
⚠ Piège : {{ valeur }}, sans parenthèses, n'affiche pas la valeur. Angular le signale : NG8109: valeur is a function and should be invoked: valeur().

❓ Questions (à répondre dans le README.md)

  1. Pourquoi la valeur doit-elle être un signal et non une propriété ordinaire (indice : Angular 22, sans zone.js) ?
  2. Quel est le type de $event dans (action)="noter($event)"? Et dans (click)="plus()", si on l'avait passé ?
  3. Pourquoi faire passer toutes les modifications par une seule méthode changer()?
git add .
git commit -m "Partie 2 : compteur numérique et historique minimal"
  1. Partie 3 : limites, couleurs et jauge

⏱ 40 min

Notions mobilisées : Le property binding · Les signals · La syntaxe de base

  • Le compteur reste entre min (−10) et max (10), deux nouveaux signals :
    • dans changer(), utilisez borner(); si la valeur ne change pas, n'émettez aucune action ;
    • créez trois computed: auMin, auMax et position (position de la valeur entre min et max, en pourcentage) ;
    • désactivez − au minimum et + au maximum avec [disabled], ainsi que Réinitialiser quand la valeur vaut 0 ;
    • colorez la valeur avec [class.positif] (vert) et [class.negatif] (rouge) ;
    • dessinez une jauge : une barre grise, un trait vertical à la position du zéro, et un curseur rond placé avec [style.left.%]="position()", qui devient rouge à une limite ;
    • affichez avec @if le message « Limite maximale atteinte (10). » ou « Limite minimale atteinte (−10). » ;
    • pour l'accessibilité, donnez à l'élément de la valeur l'attribut aria-valuenow ([attr.aria-valuenow]), et aux boutons un aria-label.

Point de contrôle de la partie 3 : la limite maximale atteinte

✅ Point de contrôle

  • Avec un pas de 5 (valeur fixe provisoire), deux clics depuis 3 donnent 8 puis 10, et non 13 : le bouton + 5 se grise, le curseur devient rouge et le message s'affiche.
  • Un nouveau clic sur + désactivé n'ajoute rien à l'historique.

❓ Questions (à répondre dans le README.md)

  1. Quelle différence y a-t-il entre [class.positif]="…" et [class]="…"? Lequel conserve la classe valeur déjà présente ?
  2. Pourquoi écrit-on [style.left.%] plutôt que [style.left]="position() + '%'"? Les deux fonctionnent-ils ?
  3. position est un computed: combien de fois est-il recalculé si l'on clique sur un bouton désactivé ?
git add .
git commit -m "Partie 3 : limites, couleurs et jauge"
  1. Partie 4 : les réglages en two-way binding

⏱ 45 min

Notions mobilisées : Le two-way data binding · Les signals

  • Ajoutez sous le compteur un <fieldset> « Réglages » :
    • une liste Pas (1, 2, 5, 10) liée par [(ngModel)]="pas", avec [ngValue] sur chaque option, générée par un @for;
    • deux champs numériques Min et Max liés par [(ngModel)] aux signals min et max;
    • un computed reglagesValides: le minimum doit être négatif et le maximum positif ; sinon, affichez un message d'erreur.
  • Problème : si la valeur vaut 8 et que l'on règle le maximum à 3, la valeur doit redescendre à 3. Transformez valeur en linkedSignal: il se recalcule quand min ou max change, en conservant la valeur précédente, bornée.
protected readonly valeur = linkedSignal<{ min: number; max: number }, number>({
  source: () => ({ min: this.min(), max: this.max() }),
  computation: (limites, precedent) => /* à compléter avec borner() */,
});

Point de contrôle de la partie 4 : des réglages invalides

✅ Point de contrôle

  • Changer le pas en 5 modifie aussitôt le texte des boutons (− 5, + 5).
  • Avec une valeur de 4, régler le maximum à 3 ramène la valeur à 3 et affiche la limite atteinte.
  • Saisir 4 comme minimum affiche « Le minimum doit être négatif et le maximum positif. ».
⚠ Piège : sans les types entre chevrons, linkedSignal({ source, computation }) ne sait pas déduire le type de la valeur : TS2571: Object is of type 'unknown'. partout où valeur() est utilisée.
⚠ Piège : oublier FormsModule dans les imports du composant donne NG8002: Can't bind to 'ngModel' since it isn't a known property of 'select'.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi utiliser [ngValue] et non value sur les options de la liste Pas? Que contiendrait pas() sinon ?
  2. Pourquoi un simple computed ne convient-il pas pour valeur? Et un effect qui corrigerait la valeur ?
  3. Quel est le type de la valeur d'un champ type="number" lié par ngModel? Que vaut-elle si le champ est vidé ?
git add .
git commit -m "Partie 4 : réglages et linkedSignal"
  1. Partie 5 : le compteur ASCII

⏱ 45 min

Notions mobilisées : Les signals · La syntaxe de base · Le property binding · Les variables de template

  • Dans CompteurAscii, un signal code (65) et des computed: caractere (String.fromCharCode), type (avec typeCaractere()), voyelle, hexa et binaire (sur 8 chiffres).
    • deux boutons ◀ et ▶ (désactivés aux limites 65 et 122) ; au-delà, un message « Limite atteinte » ;
    • le caractère en grand, coloré avec [class]="type()": bleu pour une majuscule, vert pour une minuscule, orange pour un symbole ; les voyelles sont soulignées ([class.voyelle]) ;
    • une ligne « Code 101 · hexa 0x65 · binaire 01100101 », l'hexadécimal passant par le pipe uppercase;
    • la nature du caractère avec @switch (« Lettre majuscule », « Lettre minuscule », « Symbole (entre Z et a) »), suivie d'un badge « voyelle » ;
    • un champ #saisie d'un caractère : Entrée ((keydown.enter)) ou le bouton Aller appellent chercher(saisie), qui va directement au caractère tapé, vide le champ et lui redonne le focus ; un caractère hors limites affiche un message ;
    • chaque changement émet une Action (compteur: 'ascii') : dans App, écoutez-la comme pour le compteur numérique.

Point de contrôle de la partie 5 : la lettre e

✅ Point de contrôle

  • Taper e puis Entrée affiche un e vert souligné, « Code 101 · hexa 0x65 · binaire 01100101 » et « Lettre minuscule · voyelle ».
  • De Z, le bouton ▶ mène à [, en orange : « Symbole (entre Z et a) ».
  • Taper 5 affiche « « 5 » n'est pas entre A et z. ».
  • Variante individuelle (comme dans le TP Flutter) : ajoutez une règle d'affichage de votre choix, par exemple les consonnes en gras, les lettres de votre prénom encadrées, ou une couleur différente pour chaque groupe de symboles. Décrivez-la dans le README.

❓ Questions (à répondre dans le README.md)

  1. Quels caractères se trouvent entre Z (90) et a (97) ?
  2. Pourquoi transmettre l'élément #saisie à la méthode plutôt que $event?
  3. Pourquoi utiliser @switch plutôt que trois @if?
git add .
git commit -m "Partie 5 : compteur ASCII"
  1. Partie 6 : raccourcis clavier et annulation

⏱ 30 min

Notions mobilisées : La gestion des événements · Les signals

  • Le compteur numérique se pilote aussi au clavier, sans clic préalable. Dans son décorateur, écoutez '(document:keydown)' avec la propriété host et une méthode clavier(e: KeyboardEvent):
Touche Action
↑ ou + augmenter du pas
↓ ou - diminuer du pas
0 réinitialiser
Ctrl + Z annuler la dernière modification
    • ignorez les touches tapées dans un champ (INPUT, SELECT, TEXTAREA) : on doit pouvoir saisir « -5 » dans Min sans modifier le compteur ;
    • appelez preventDefault() pour les flèches (sinon la page défile) et pour Ctrl + Z ;
    • annulation: un signal privé precedentes (tableau des 20 dernières valeurs) est complété par changer(); annuler() revient à la dernière valeur et émet une action 'annuler'; un bouton Annuler (n) indique le nombre d'annulations possibles ;
    • dans CompteurAscii, utilisez cette fois les filtres de touches: '(document:keydown.arrowleft)' et '(document:keydown.arrowright)'.

✅ Point de contrôle

  • Après un clic dans le vide de la page : ↑ ↑ ↑ donne 3, Ctrl + Z donne 2, 0 remet à zéro.
  • Dans le champ Max, la flèche ↑ modifie le champ, pas le compteur.
  • ← et → changent le caractère ASCII, même quand le compteur numérique a été utilisé juste avant.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi l'écouteur est-il placé sur document et non sur le composant ?
  2. Pourquoi tester e.key dans une méthode pour le compteur numérique, alors qu'un filtre suffit pour le compteur ASCII ?
  3. Pourquoi precedentes est-il limité à 20 valeurs ?
git add .
git commit -m "Partie 6 : clavier et annulation"
  1. Partie 7 : l'historique

⏱ 45 min

Notions mobilisées : Les décorateurs · La syntaxe de base · Les signals · Les variables de template

  • Le composant Historique reçoit les actions de App: actions = input.required<Action[]>(), et prévient le parent avec vider = output<void>(). Dans App: <app-historique [actions]="historique()" (vider)="historique.set([])" />.
    • computed: recentes (les 8 dernières, de la plus récente à la plus ancienne), numeriques, plusHaute et plusBasse (valeurs extrêmes atteintes) ;
    • cinq statistiques : nombre total d'actions (avec @let total = actions().length;), numériques, ASCII, plus haut, plus bas ;
    • la liste avec @for (a of recentes(); track a.id; let premiere = $first, i = $index): un numéro (total - i), le libellé de l'opération, « avant → après » (des caractères pour l'ASCII), l'heure avec le pipe date: 'HH:mm:ss'; la première ligne est mise en valeur ; @empty affiche « Aucune action pour l'instant » ;
    • sous la liste : « … et N action(s) plus ancienne(s) » s'il y en a plus de 8 ;
    • enregistrement: dans le constructeur de App, un effect enregistre les 50 dernières actions dans localStorage (dans un try … catch) ; au démarrage, historique est initialisé en relisant ces données, et chaque heure est reconvertie en Date.
  • Pour afficher les dates en français, ajoutez dans app.config.ts: registerLocaleData(localeFr) et le fournisseur { provide: LOCALE_ID, useValue: 'fr' }.

Point de contrôle de la partie 7 : l'historique et ses statistiques

✅ Point de contrôle

  • Chaque action des deux compteurs apparaît en haut de la liste, avec son heure.
  • Après un rechargement de la page (F5), l'historique et les statistiques sont toujours là.
  • Vider affiche « Aucune action pour l'instant » et l'en-tête revient à 0.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi faut-il recréer les objets Date après JSON.parse? Que se passerait-il avec le pipe date sinon ?
  2. Pourquoi utiliser un effect pour l'enregistrement, et non un computed?
  3. Pourquoi Historique ne modifie-t-il pas lui-même la liste, mais émet vider?
git add .
git commit -m "Partie 7 : historique et enregistrement"
  1. Partie 8 : un composant réutilisable

⏱ 45 min

Notions mobilisées : Les décorateurs · Le two-way data binding · La gestion des événements · Les variables de template

  • MiniCompteur est un petit compteur à objectif, conçu pour être réutilisé :
    • entrées : libelle (obligatoire), icone (« • » par défaut), max (10 par défaut, converti avec numberAttribute) ;
    • valeur = model(0): une valeur que le parent fournit et que l'enfant modifie ;
    • sorties : limiteAtteinte (émet le libellé quand la valeur atteint max) et supprimer;
    • affichage : l'icône, le libellé, une barre de progression ([style.width.%]), les boutons − et + (bornés à 0 et max), « 3 / 8 » et un bouton × ; la ligne passe en vert quand l'objectif est atteint.
  • MesCompteurs affiche une liste de suivis (signal de tableau { id, libelle, icone, max, valeur }, avec trois suivis de départ) :
    • un @for de <app-mini-compteur>, avec [valeur]="s.valeur" et (valeurChange)="changer(s.id, $event)";
    • dans l'en-tête, le total et l'objectif global (« 14 / 29 ») ; en bas, « N objectif(s) atteint(s) sur M » et le message « Bravo : objectif « … » atteint ! » ;
    • un formulaire d'ajout avec deux variables de référence, #nom et #max: Entrée dans le nom ou le bouton Ajouter appellent ajouter(nom, max), qui vide le champ et lui redonne le focus.

Point de contrôle de la partie 8 : quatre compteurs du jour

✅ Point de contrôle

  • Cinq clics sur + de « Verres d'eau » (3 / 8) mènent à 8 / 8 : la ligne devient verte, le + se grise, le message « Bravo » s'affiche et le total passe de 4 à 9.
  • L'ajout de « Pages lues » (objectif 12) crée une ligne ⭐ 0 / 12 ; × supprime une ligne et met le total à jour.
⚠ Piège : écrire [(valeur)]="s.valeur" dans le @for compile, mais modifie l'objet sur place: le mini-compteur affiche 5 / 8 alors que le total reste à 4 / 17, car le signal suivis n'est pas prévenu. Il faut créer un nouveau tableau dans changer().

❓ Questions (à répondre dans le README.md)

  1. Quelles deux liaisons model() crée-t-il pour le parent ? Écrivez l'équivalent « long » de [(valeur)].
  2. Pourquoi MiniCompteur ne connaît-il pas la liste des suivis ? Quel avantage pour sa réutilisation ?
  3. À quoi sert track s.id quand on supprime une ligne au milieu de la liste ?
git add .
git commit -m "Partie 8 : composant réutilisable"
  1. Partie 9 : variantes et finitions

⏱ 15 min

  • Choisissez deux variantes (chaque binôme annonce ses choix au formateur, pour varier les projets) :
    • un champ Pas personnalisé (de 1 à 50) en plus de la liste ;
    • un thème sombre, mémorisé dans localStorage avec un effect;
    • une animation (classe CSS temporaire) à chaque changement de valeur ;
    • un export de l'historique au format CSV, dans une zone de texte à copier ;
    • un objectif modifiable directement dans chaque mini-compteur ;
    • une variante libre, validée par le formateur.
  • Relisez votre code : noms explicites, membres protected ou private quand ils ne servent pas au parent, aucun console.log oublié, aucune erreur dans la console.
git add .
git commit -m "Partie 9 : variantes et finitions"
  1. Grille d'évaluation (sur 20)

Critère Points
Partie 0–1 : projet, structure, modèle TypeScript typé (unions, interfaces, fonctions) 2
Partie 2–3 : compteur numérique, signals, limites, liaisons de propriété, de classe et de style 3
Partie 4 : réglages en two-way binding, linkedSignal, validation 2
Partie 5 : compteur ASCII, computed, @switch, pipe, #saisie, variante individuelle 3
Partie 6 : raccourcis clavier, host, annulation 2
Partie 7 : historique, input / output, @for, @let, persistance 3
Partie 8 : composant réutilisable avec model(), liste et formulaire d'ajout 2
Qualité : lisibilité, typage strict, aucune erreur ni avertissement, un commit par partie 1
README : réponses aux questions et description des variantes 2
Total 20
  1. Livrables

    • Le lien du dépôt Git (GitHub, GitLab…), avec au moins dix commits nommés « Partie N : … ».
    • Le fichier README.md: vos noms, les réponses aux questions de chaque partie, les deux variantes choisies et une capture d'écran de l'application terminée.
    • Le projet doit se lancer avec npm install puis npx ng serve, sans erreur ni avertissement.
  1. Erreurs fréquentes

Message ou symptôme Cause Solution
TS2571: Object is of type 'unknown'. sur valeur() linkedSignal (forme longue) sans types linkedSignal<{ min: number; max: number }, number>({ … })
NG8002: Can't bind to 'ngModel' since it isn't a known property of 'select'. FormsModule absent des imports du composant L'ajouter au tableau imports
NG5002: @for loop must have a "track" expression @for sans track track p, track a.id…
TS2339: Property 'Math' does not exist on type 'CompteurNumerique'. Math utilisé dans le template Faire le calcul dans un computed
NG8109: valeur is a function and should be invoked: valeur() Signal sans parenthèses dans le template {{ valeur() }}
Le total des mini-compteurs ne change pas [(valeur)]="s.valeur": objet modifié sur place (valeurChange) et un nouveau tableau avec update()
La page défile quand on utilise les flèches preventDefault() oublié L'appeler dans clavier()
Le compteur change pendant la saisie dans Min Les touches tapées dans un champ ne sont pas ignorées Tester e.target.tagName
Après F5, l'heure n'est plus une date ou l'historique est vide heure restée en chaîne, ou erreur non gérée dans lire() new Date(a.heure) et try … catch
pas() contient « 5 » au lieu de 5 value au lieu de [ngValue] sur les options [ngValue]="p"
  1. Pour aller plus loin

    • Déplacer l'historique dans un service injecté avec inject(): c'est l'objet du chapitre « Les services et l'injection de dépendances ».
    • Remplacer Historique et MesCompteurs par deux pages accessibles depuis un menu : ce sera l'objet du chapitre sur le routage.
    • Écrire un test unitaire pour borner() et typeCaractere() avec npx ng test.
  1. Bilan

    • Un modèle TypeScript partagé rend toutes les données explicites et vérifiées.
    • Les signals portent l'état ; computed en déduit tout le reste ; linkedSignal et effect traitent les cas particuliers.
    • Les liaisons relient la classe et la vue : {{ }}, [ ], ( ), [( )].
    • Les composants communiquent par input(), output() et model(), sans se connaître.
    • Les variables de template et le contrôle de flux gardent les templates courts et lisibles.
Un bon compteur n'est pas celui qui compte le plus vite, mais celui dont chaque valeur affichée découle, sans surprise, d'une donnée claire : c'est tout l'esprit d'Angular moderne.