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

TP 3 : réaliser une TodoList avec Angular

L’application « Mes tâches » permet d’ajouter, cocher, modifier, filtrer, rechercher et supprimer des tâches. Elle a des priorités, des échéances avec signalement du retard, une barre de progression, ...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

L’application « Mes tâches » permet d’ajouter, cocher, modifier, filtrer, rechercher et supprimer des tâches. Elle a des priorités, des échéances avec signalement du retard, une barre de progression, une boîte de confirmation, des raccourcis clavier et une sauvegarde dans le navigateur. Le TP applique une règle unique : un seul composant possède les données, les autres les reçoivent par input() et annoncent les actions par output(), relayées d’un niveau à l’autre. Chacune des 10 parties se termine par un point de contrôle, des questions et un commit, et le TP fournit une grille d’évaluation et un corrigé complet.

🎬 La correction du TP en vidéo : la TodoList construite partie par partie (17 min 11)

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

Durée Niveau Outils Livrable
6 heures (10 parties) Débutant, après le chapitre « Interaction entre composants » 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

    • Construire une application Angular complète, découpée en sept composants qui ont chacun un rôle unique
    • Appliquer la règle d'or de la communication : les données descendent par input(), les événements remontent par output(), un seul composant possède les données
    • Relayer des événements sur plusieurs niveaux, et modifier un tableau d'objets de façon immuable
    • Utiliser model() pour une liaison bidirectionnelle entre composants
    • Commander un enfant avec viewChild(): appeler ses méthodes, accéder au DOM (focus(), <dialog>)
    • Réinvestir les signals (computed, linkedSignal, effect), les événements clavier et les variables de template
    • 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 Mes tâches, une liste de tâches (TodoList) complète :
      • ajouter une tâche avec un titre, une priorité (haute, normale, basse) et une échéance facultative ;
      • cocher, modifier (double-clic) et supprimer une tâche ; une tâche dont l'échéance est passée est signalée « En retard » ;
      • filtrer (toutes, actives, terminées), rechercher, tout terminer, supprimer les tâches terminées après confirmation;
      • suivre sa progression, utiliser des raccourcis clavier, et retrouver ses tâches après un rechargement de la page.
    • La liste est toujours triée : tâches actives d'abord, puis par priorité, puis par échéance.

Image de référence : l'application Mes tâches terminée

Architecture de l'application : sept composants, un modèle et des données qui circulent par input / output

  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 : …".
    • Les aides donnent des squelettes de code, jamais la solution complète : c'est à vous de compléter.
    • Une règle s'applique à tout le TP : seul App modifie la liste des tâches. Les autres composants reçoivent des données et émettent des événements.

Déroulement du TP sur 6 heures

Notions du chapitre réinvesties dans chaque partie du TP

  1. Partie 0 : mise en place du projet

⏱ 20 min

Notions mobilisées : Les composants · La syntaxe de base

  • Créez le projet, générez les six composants enfants et le dossier du modèle :
npx @angular/cli@latest new tp3-todolist --defaults --ssr=false --style=css
cd tp3-todolist
npx ng generate component formulaire-tache
npx ng generate component barre-filtres
npx ng generate component liste-taches
npx ng generate component item-tache
npx ng generate component progression
npx ng generate component boite-confirmation
mkdir src/app/modeles
npx ng serve
  • Dans app.html, placez un en-tête (<h1>Mes tâches</h1>) et une carte <main class="carte-tp"> qui contiendra le formulaire, la barre de filtres et la liste.
  • Dans styles.css, définissez les variables de couleur (--bleu, --vert, --rouge, --gris, --bord), un style commun pour les boutons (avec les variantes .secondaire, .danger et .icone) et la classe .carte-tp.
  • Dans app.config.ts, activez le français pour le pipe date: registerLocaleData(localeFr) et { provide: LOCALE_ID, useValue: 'fr' }.
src/app/
├── app.ts / app.html / app.css        ← possède les données (toutes les parties)
├── modeles/tache.ts                   ← partie 1
├── liste-taches/  item-tache/         ← parties 2, 4 et 6
├── formulaire-tache/                  ← partie 3
├── barre-filtres/                     ← partie 5
├── progression/  boite-confirmation/  ← partie 7

✅ Point de contrôle

  • ng serve fonctionne, la page affiche le titre « Mes tâches » et la carte blanche.
  • La console du navigateur (F12) ne contient aucune erreur.

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

  1. Combien de fichiers ng generate component crée-t-il ? À quoi sert chacun ?
  2. Pourquoi crée-t-on six composants au lieu de tout écrire dans App? Citez deux avantages.
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 : Le typage fort

  • Dans src/app/modeles/tache.ts, sans aucun code Angular :
    • deux types : Priorite ('haute', 'normale', 'basse') et Filtre ('toutes', 'actives', 'terminees') ;
    • une interface Tache dont toutes les propriétés sont readonly: id, titre, priorite, echeance (string | null, au format AAAA-MM-JJ), terminee, creeeLe (Date) ;
    • un type NouvelleTache, construit avec Pick<Tache, …>, qui ne garde que ce que l'utilisateur saisit ;
    • une interface Compteurs: toutes, actives, terminees; une constante PRIORITES;
    • quatre fonctions : creerTache(n) (identifiant crypto.randomUUID(), titre nettoyé, non terminée), dateDuJour(decalage = 0), estEnRetard(tache, aujourdhui) et trierTaches(taches).
export interface Tache {
  readonly id: string;
  /* à compléter : titre, priorite, echeance, terminee, creeeLe */
}

export type NouvelleTache = Pick<Tache, /* à compléter */>;

// Tâches actives d'abord, puis par priorité, puis par échéance (sans échéance à la fin)
export function trierTaches(taches: readonly Tache[]): Tache[] {
  return [...taches].sort((a, b) =>
    Number(a.terminee) - Number(b.terminee) || /* priorité */ || /* échéance */);
}
  • Les dates au format AAAA-MM-JJ se comparent comme du texte : '2026-10-29' < '2026-10-31' vaut true. Pour dateDuJour(), construisez la chaîne avec getFullYear(), getMonth() + 1 et getDate(), complétés par padStart(2, '0').

✅ Point de contrôle

  • estEnRetard renvoie true pour une tâche active d'échéance hier, false si elle est terminée ou sans échéance.
  • trierTaches place une tâche « basse » sans échéance après une tâche « haute » datée, et toute tâche terminée en dernier.
⚠ Piège : avec des propriétés readonly, écrire tache.terminee = true est refusé : TS2540: Cannot assign to 'terminee' because it is a read-only property. C'est voulu : on crée toujours une nouvelle tâche ({ ...t, terminee: true }).

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

  1. Pourquoi trierTaches commence-t-il par [...taches] au lieu de trier directement le tableau reçu ?
  2. Pourquoi ne pas utiliser toISOString().slice(0, 10) pour la date du jour ? (indice : fuseau horaire, autour de minuit)
  3. Quel est l'intérêt de Pick plutôt que de réécrire une seconde interface ?
git add .
git commit -m "Partie 1 : modèle TypeScript"
  1. Partie 2 : afficher la liste avec input()

⏱ 40 min

Notions mobilisées : Transmettre des données avec input() · La communication entre composants · La syntaxe de base

  • Dans App: un signal taches initialisé avec quatre tâches d'exemple (dont une terminée et une en retard), une propriété aujourdhui = dateDuJour() et un computed visibles qui renvoie la liste triée.
    • ListeTaches: deux entrées obligatoires, taches et aujourdhui; un @for … track t.id qui affiche un <app-item-tache> par tâche, et @empty: « Aucune tâche à afficher. » ;
    • ItemTache: entrées obligatoires tache et aujourdhui; un computed enRetard; affichage d'une case à cocher ([checked]), du titre (barré si la tâche est terminée), d'un badge de priorité coloré ([class]), de l'échéance avec le pipe date: 'd MMM', précédée de « En retard · » en rouge si besoin, et de deux boutons ✎ et ✕ (sans action pour l'instant).
export class ItemTache {
  readonly tache = input.required<Tache>();
  readonly aujourdhui = input.required<string>();
  protected readonly enRetard = computed(() => /* à compléter */);
}

Point de contrôle de la partie 2 : la liste triée

✅ Point de contrôle

  • Les quatre tâches s'affichent dans l'ordre : « Rendre le TP 2 » (haute, en retard), « Préparer le quiz » (haute), « Tester viewChild » (basse, sans date), puis la tâche terminée, barrée.
  • Remplacer le tableau par [] affiche « Aucune tâche à afficher. ».
⚠ Piège : oublier [aujourdhui] sur <app-liste-taches> est détecté à la compilation : NG8008: Required input 'aujourdhui' from component ListeTaches must be specified. Et oublier DatePipe dans les imports de ItemTache donne NG8004: No pipe found with name 'date'.

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

  1. Pourquoi enRetard est-il calculé dans ItemTache et non stocké dans la tâche ?
  2. Pourquoi passer aujourdhui en entrée plutôt que d'appeler dateDuJour() dans chaque ligne ?
  3. À quoi sert track t.id? Que se passerait-il avec track $index quand la liste est triée de nouveau ?
git add .
git commit -m "Partie 2 : affichage de la liste"
  1. Partie 3 : ajouter une tâche avec output()

⏱ 45 min

Notions mobilisées : Émettre des événements avec output() · Les variables de template · La gestion des événements

  • FormulaireTache ne connaît pas la liste : il émet la tâche saisie avec ajouter = output<NouvelleTache>().
    • un <form> avec trois champs marqués par des variables de référence : #titre, #priorite (une liste générée avec @for sur PRIORITES, « normale » sélectionnée par défaut) et #echeance (type="date") ;
    • (submit) empêche le rechargement ($event.preventDefault()) et appelle valider(titre, priorite, echeance);
    • un titre de moins de 3 caractères affiche « Le titre doit comporter au moins 3 caractères. » (signal erreur), effacé dès la frappe suivante ;
    • sinon : émettre, vider le titre et l'échéance, et redonner le focus au titre pour enchaîner les saisies.
  • Dans App: (ajouter)="ajouter($event)" crée la tâche avec creerTache() et l'ajoute avec update(). Un signal annonce affiche sous la liste « « … » ajoutée. » (avec aria-live="polite").
protected valider(titre: HTMLInputElement, priorite: HTMLSelectElement, echeance: HTMLInputElement): void {
  const texte = titre.value.trim();
  // 1. vérifier   2. émettre { titre, priorite, echeance }   3. vider et redonner le focus
}

Point de contrôle de la partie 3 : un titre trop court

✅ Point de contrôle

  • « ab » + Entrée affiche le message d'erreur, et rien n'est ajouté.
  • « Lire la doc des signals », priorité haute, échéance dans quelques jours : la tâche apparaît à sa place dans la liste triée, le champ est vidé et garde le focus.
⚠ Piège : (ajouter)="ajouter()", sans $event, est refusé si la méthode attend la tâche : TS2554: Expected 1 arguments, but got 0.

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

  1. Pourquoi le formulaire émet-il une NouvelleTache et non une Tache complète ?
  2. Quel est le type de $event dans (ajouter)="ajouter($event)"? Et dans (submit)?
  3. Pourquoi transmettre les éléments #titre, #priorite et #echeance à la méthode plutôt que d'utiliser trois signals ?
git add .
git commit -m "Partie 3 : formulaire d'ajout"
  1. Partie 4 : cocher et supprimer, relayer les événements

⏱ 40 min

Notions mobilisées : Émettre des événements avec output() · La communication entre composants

  • Un clic dans une ligne doit modifier la liste, qui appartient à App, deux niveaux plus haut. Les sorties ne remontant que d'un niveau, il faut les relayer:
    • ItemTache émet basculer et supprimer, sans valeur : la ligne ne connaît que sa tâche ;
    • ListeTaches relaie avec ses propres sorties, en ajoutant l'identifiant : (basculer)="basculer.emit(t.id)";
    • App modifie la liste avec map (basculer) et filter (supprimer), et annonce « « … » supprimée. ».
protected basculer(id: string): void {
  this.taches.update((l) => l.map((t) => (t.id === id ? /* nouvelle tâche */ : t)));
}

Point de contrôle de la partie 4 : une tâche cochée, une tâche supprimée

✅ Point de contrôle

  • Cocher « Rendre le TP 2 » la barre, la fait descendre parmi les terminées, et fait disparaître « En retard ».
  • ✕ sur une ligne la supprime et affiche l'annonce. Aucune autre ligne ne change.
⚠ Piège : modifier la tâche sur place (t.terminee = !t.terminee, si vous avez retiré readonly) ou utiliser push / splice ne crée pas de nouveau tableau : le signal n'est pas prévenu, le tri et les compteurs ne se mettent pas à jour.

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

  1. Dessinez le trajet d'un clic sur ✕, de ItemTache jusqu'à App, avec le nom de chaque sortie et la valeur transportée.
  2. Pourquoi ItemTache n'émet-il pas l'identifiant lui-même ? Serait-ce une erreur ?
  3. Quel autre outil du chapitre éviterait de relayer, si les niveaux étaient très nombreux ?
git add .
git commit -m "Partie 4 : cocher et supprimer"
  1. Partie 5 : filtres, recherche et model()

⏱ 40 min

Notions mobilisées : La communication entre composants · Le two-way data binding · Les signals

  • BarreFiltres:
    • filtre = model.required<Filtre>() et recherche = model(''): le parent les relie avec [(filtre)]="filtre" et [(recherche)]="recherche";
    • une entrée compteurs (Compteurs) : chaque bouton de filtre affiche son nombre (« Actives 3 ») ; le bouton actif est en bleu ;
    • un champ de recherche #champ qui met à jour recherche à chaque frappe ;
    • deux sorties : toutTerminer et viderTerminees, avec deux boutons « Tout terminer » et « Supprimer les terminées (n) », désactivés s'il n'y a rien à faire.
  • Dans App: des signals filtre et recherche, un computed compteurs, et visibles qui filtre maintenant selon le filtre et la recherche (sans tenir compte des majuscules) avant de trier. Pour l'instant, (viderTerminees) supprime directement les terminées : la confirmation viendra à la partie 7.

Point de contrôle de la partie 5 : le filtre Actives et la recherche « tp »

✅ Point de contrôle

  • « Actives » n'affiche que les tâches non cochées ; les nombres des trois boutons sont justes et se mettent à jour quand on coche.
  • Avec « Actives » et la recherche « tp », seules « Rendre le TP 2 » et « Préparer le quiz sur output() » restent.
  • « Tout terminer » coche toutes les tâches puis se désactive.
⚠ Piège : [(filtre)]="filtre()" (avec parenthèses) est refusé : NG5002: Unsupported expression in a two-way binding. On passe le signal lui-même : [(filtre)]="filtre". Et filtre.set('fini') donne TS2345: Argument of type '"fini"' is not assignable to parameter of type 'Filtre'.

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

  1. Écrivez l'équivalent « long » de [(filtre)]="filtre", avec une entrée et une sortie.
  2. Pourquoi compteurs est-il calculé dans App et transmis en entrée, plutôt que calculé dans BarreFiltres?
  3. Pourquoi « Supprimer les terminées » est-il une sortie, alors que le filtre est un model()?
git add .
git commit -m "Partie 5 : filtres et recherche"
  1. Partie 6 : modifier une tâche, linkedSignal et viewChild

⏱ 45 min

Notions mobilisées : Accéder à un composant enfant avec viewChild · Les signals · La gestion des événements · Les variables de template

  • Un double-clic sur le titre, ou le bouton ✎, passe la ligne en mode édition (désactivé pour une tâche terminée) :
    • un signal enEdition; avec @if, le titre est remplacé par un champ #champ;
    • le texte saisi est un brouillon: brouillon = linkedSignal(() => this.tache().titre), modifiable, et remis au titre reçu si la tâche change ;
    • Entrée ou la perte du focus (blur) valident : si le brouillon nettoyé fait au moins 3 caractères et diffère du titre, la ligne émet renommer (le nouveau titre) ; Échap annule ;
    • le champ doit recevoir le focus, avec son texte sélectionné, dès son apparition : récupérez-le avec viewChild<ElementRef<HTMLInputElement>>('champ') (sans .required: il n'existe que pendant l'édition) et un effect.
  • ListeTaches relaie : (renommer)="renommer.emit({ id: t.id, titre: $event })"; App met à jour le titre et annonce « Tâche renommée en « … ». ».
private readonly champ = viewChild<ElementRef<HTMLInputElement>>('champ');

constructor() {
  effect(() => {
    const c = this.champ()?.nativeElement;
    // si le champ existe : lui donner le focus et sélectionner son texte
  });
}

Point de contrôle de la partie 6 : une tâche en cours de modification

✅ Point de contrôle

  • Double-clic sur « Tester viewChild sur un formulaire » : le champ apparaît, texte sélectionné ; taper un nouveau titre puis Entrée le renomme.
  • Modifier puis Échap : le titre d'origine revient. Vider le champ puis cliquer ailleurs : le titre d'origine revient aussi.
  • Le bouton ✎ d'une tâche terminée est grisé.

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

  1. Pourquoi un linkedSignal plutôt qu'un simple signal(this.tache().titre)? (indice : NG8118, et le renommage)
  2. Pourquoi la requête champ ne peut-elle pas être viewChild.required?
  3. Pourquoi l'effect est-il relancé à chaque passage en mode édition ?
git add .
git commit -m "Partie 6 : modification d'une tâche"
  1. Partie 7 : progression et confirmation

⏱ 45 min

Notions mobilisées : Transmettre des données avec input() · Émettre des événements avec output() · Accéder à un composant enfant avec viewChild · Les composants

  • Progression, composant d'affichage pur, placé dans l'en-tête :
    • deux entrées faites et total (avec numberAttribute) ; computed: pourcent et message (« Aucune tâche », « 2 restante(s) », « Tout est fait ! ») ;
    • une barre avec [style.width.%], verte à 100 %, et les attributs role="progressbar" et [attr.aria-valuenow].
  • BoiteConfirmation, autour de la balise native <dialog>:
    • entrées titre et libelleConfirmer; le message est projeté avec <ng-content />;
    • une méthode publique ouvrir(), qui appelle showModal() sur le <dialog> obtenu par viewChild.required;
    • Annuler ferme ; le bouton rouge ferme et émet confirmer.
  • Dans App: confirmation = viewChild.required(BoiteConfirmation); (viderTerminees) appelle maintenant demanderVidage(), qui ouvre la boîte ; (confirmer) supprime les terminées et annonce « 2 tâche(s) terminée(s) supprimée(s). ».
<app-boite-confirmation titre="Supprimer les tâches terminées ?" libelleConfirmer="Supprimer" (confirmer)="viderTerminees()">
  <p>{{ compteurs().terminees }} tâche(s) terminée(s) seront supprimées définitivement.</p>
</app-boite-confirmation>

Point de contrôle de la partie 7 : la confirmation ouverte et la progression

✅ Point de contrôle

  • L'en-tête affiche « 1 / 4 terminée(s) · 3 restante(s) » et la barre avance quand on coche ; tout cocher la rend verte avec « Tout est fait ! ».
  • « Supprimer les terminées (1) » ouvre la boîte (fond assombri) ; Annuler ou Échap ne suppriment rien ; « Supprimer » vide les terminées.
⚠ Piège : this.dialogue().showModal() est refusé : TS2339: Property 'showModal' does not exist on type 'ElementRef<HTMLDialogElement>'. La balise est dans nativeElement.

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

  1. Pourquoi le message de la boîte est-il projeté, au lieu d'être une entrée message?
  2. Pourquoi une entrée [ouverte] ne suffirait-elle pas pour afficher un <dialog> modal ?
  3. Progression ne contient ni sortie ni méthode publique : comment qualifier ce type de composant ?
git add .
git commit -m "Partie 7 : progression et confirmation"
  1. Partie 8 : raccourcis clavier et sauvegarde

⏱ 35 min

Notions mobilisées : Accéder à un composant enfant avec viewChild · La gestion des événements · Les signals

  • Raccourcis globaux, écoutés dans App avec host: { '(document:keydown)': 'clavier($event)' }:
Touche Action
n placer le curseur dans le titre du formulaire
/ placer le curseur dans la recherche
    • ignorez les touches tapées dans un champ (sinon on ne pourrait plus écrire un « n ») et les combinaisons avec Ctrl, Alt ou Cmd ;
    • App obtient les enfants avec viewChild.required(FormulaireTache) et viewChild.required(BarreFiltres), et appelle leurs méthodes publiques focaliser() et focaliserRecherche() (chacune utilise un viewChild sur son champ) ;
    • appelez preventDefault(): sinon le « n » serait aussi tapé dans le champ qui vient de recevoir le focus.
  • Sauvegarde: un effect enregistre taches() dans localStorage (dans un try … catch) ; au démarrage, taches est initialisé par une méthode lire() qui relit ces données, reconvertit creeeLe en Date, et renvoie les exemples s'il n'y a rien.

✅ Point de contrôle

  • Après un clic dans le vide : n place le curseur dans le titre, sans y écrire de « n » ; / place le curseur dans la recherche.
  • Dans le titre, taper « n » écrit bien la lettre.
  • Après F5, les tâches, leur état et leurs titres modifiés sont toujours là.
⚠ Piège : lire une requête obligatoire dans le constructeur (this.formulaire().focaliser()) est refusé : NG8118: `formulaire` is a required `viewChild` and does not have a value in this context. Dans clavier(), appelé après l'affichage, il n'y a aucun problème.

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

  1. Pourquoi App passe-t-il par une méthode de l'enfant au lieu d'accéder directement à son champ ?
  2. Pourquoi l'enregistrement est-il un effect et non un computed?
  3. Que contiendrait creeeLe après JSON.parse sans reconversion ?
git add .
git commit -m "Partie 8 : clavier et sauvegarde"
  1. Partie 9 : variantes et finitions

⏱ 20 min

  • Choisissez deux variantes (chaque binôme annonce ses choix au formateur) :
    • des catégories (Cours, Maison, Sport…) avec un filtre supplémentaire ;
    • un bouton « Annuler la suppression » pendant 5 secondes après chaque suppression ;
    • une échéance modifiable en mode édition ;
    • un tri au choix (priorité, échéance, date de création, ordre alphabétique) ;
    • une confirmation avant de supprimer une tâche non terminée, avec la même BoiteConfirmation;
    • une variante libre, validée par le formateur.
  • Relisez votre code : membres protected ou private quand ils ne servent pas au parent, noms explicites, aucun console.log, aucune erreur dans la console.
git add .
git commit -m "Partie 9 : variantes et finitions"
  1. Grille d'évaluation (sur 20)

Critère Points
Parties 0–1 : projet, découpage en composants, modèle typé (readonly, Pick, fonctions) 2
Partie 2 : liste avec input.required, @for, @empty, tri, retard 2
Partie 3 : formulaire, validation, output(), variables de référence 2
Partie 4 : sorties relayées, mises à jour immuables 3
Partie 5 : filtres et recherche avec model(), compteurs 2
Partie 6 : édition avec linkedSignal, viewChild facultatif, Entrée / Échap / blur 3
Partie 7 : progression, boîte de confirmation (<dialog>, projection, viewChild) 2
Partie 8 : raccourcis clavier par viewChild, sauvegarde 1
Qualité : un seul composant modifie les données, typage strict, aucune erreur, 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, avec au moins dix commits nommés « Partie N : … ».
    • Le fichier README.md: vos noms, les réponses aux questions, 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
NG8008: Required input 'aujourdhui' from component ListeTaches must be specified. Entrée obligatoire oubliée Ajouter [aujourdhui]="aujourdhui"
NG8004: No pipe found with name 'date'. DatePipe non importé L'ajouter aux imports de ItemTache
TS2540: Cannot assign to 'terminee' because it is a read-only property. Tâche modifiée sur place { ...t, terminee: !t.terminee }
TS2554: Expected 1 arguments, but got 0. $event oublié dans (ajouter) (ajouter)="ajouter($event)"
NG5002: Unsupported expression in a two-way binding [(filtre)]="filtre()" [(filtre)]="filtre"
TS2345: Argument of type '"fini"' is not assignable to parameter of type 'Filtre'. Valeur hors de l'union Utiliser une valeur de Filtre
TS2339: Property 'showModal' does not exist on type 'ElementRef<HTMLDialogElement>'. nativeElement oublié this.dialogue().nativeElement.showModal()
NG8118: `formulaire` is a required `viewChild` and does not have a value in this context. Requête lue dans le constructeur La lire dans une méthode appelée plus tard
Cliquer sur ✕ ou cocher ne change rien Sortie non relayée par ListeTaches, ou tableau modifié sur place Relayer avec emit(t.id); map / filter
Le champ d'édition n'a pas le focus Focus demandé avant que le champ existe viewChild facultatif + effect
Taper « n » dans un champ ouvre le formulaire Touches des champs non ignorées Tester e.target.tagName
Après F5, plus de tâches ou erreur de date localStorage mal relu try … catch et new Date(t.creeeLe)
  1. Pour aller plus loin

    • Déplacer la liste et ses méthodes dans un service TachesService injecté avec inject(): c'est l'objet du chapitre « Les services et l'injection de dépendances ».
    • Afficher les tâches actives et terminées sur deux pages, avec le routage.
    • Écrire des tests unitaires pour trierTaches() et estEnRetard() avec npx ng test.
  1. Bilan

    • Chaque composant a un rôle : saisir, filtrer, afficher une ligne, afficher la liste, montrer la progression, confirmer.
    • Un seul composant possède les données ; les autres les reçoivent par input() et annoncent les actions par output(), relayées niveau par niveau.
    • model() crée une liaison bidirectionnelle propre entre un parent et un enfant.
    • viewChild() sert à commander: donner le focus, ouvrir une boîte de dialogue, appeler une méthode publique.
    • Les mises à jour immuables (map, filter, ...) gardent tous les computed justes.
Une bonne TodoList ne se juge pas au nombre de ses fonctions, mais à la clarté du chemin que suit chaque donnée : du parent qui la possède jusqu'à l'enfant qui l'affiche, et retour.