Après une actualisation, un état conservé uniquement en mémoire disparaît : votre liste de tâches redevient vide. Dans cette leçon, vous enregistrez le tableau des tâches dans le navigateur et le restaurez au démarrage, sans serveur ni dépendance externe.
La leçon précédente a établi le tableau d’objets comme source de l’affichage. Vous conservez cette organisation : à chaque ajout, basculement ou suppression, vous mettez à jour le tableau, puis vous enregistrez son contenu. Au démarrage, vous chargez d’abord les données enregistrées, puis vous reconstruisez la liste depuis cet état.
Enregistrer un tableau dans le navigateur localStorage associe des clés à des valeurs conservées dans le navigateur, y compris après la fermeture de l’onglet. Ses valeurs sont des chaînes de caractères : pour y sauvegarder un tableau d’objets, vous le convertissez avec JSON.stringify(). Vous le récupérez ensuite avec getItem(), puis JSON.parse() reconstruit le tableau à partir de la chaîne JSON.
JavaScript
const taches = [
{ id: "tache-1", title: "Préparer la réunion", completed: false }
];
localStorage.setItem("atelier-taches", JSON.stringify(taches));
const donneesEnregistrees = localStorage.getItem("atelier-taches");
const tachesRestaurees = JSON.parse(donneesEnregistrees);
console.log(tachesRestaurees); Aperçu du rendu Relancer Arrêter Ouvrir
const taches = [
{ id: "tache-1", title: "Préparer la réunion", completed: false }
];
localStorage.setItem("atelier-taches", JSON.stringify(taches));
const donneesEnregistrees = localStorage.getItem("atelier-taches");
const tachesRestaurees = JSON.parse(donneesEnregistrees);
console.log(tachesRestaurees); <style>:root{--atelier-couleur-texte:#202124;--atelier-couleur-fond:#f5f6fa;--atelier-couleur-accent:#3157c8;--atelier-espace-petit:0.5rem;--atelier-espace-moyen:1rem;--atelier-espace-grand:2rem;--atelier-rayon:0.5rem;--atelier-police:system-ui, sans-serif;--atelier-largeur-page:48rem;--atelier-ombre:0 0.125rem 0.5rem #20212422;}</style><!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gestionnaire de tâches</title>
<link rel="stylesheet" href="style.css">
<script type="module" src="js/main.js"></script>
</head>
<body class="atelier-page">
<header class="atelier-entete">
<h1>Gestionnaire de tâches</h1>
<p>Organisez vos tâches et suivez votre progression.</p>
</header>
<main class="atelier-principal">
<section class="atelier-progression" aria-labelledby="titre-progression">
<h2 id="titre-progression">Progression</h2>
<p><span data-progression-texte>0 tâche terminée sur 0</span></p>
<progress class="atelier-progression__barre" data-progression max="100" value="0">0%</progress>
</section>
<form class="atelier-formulaire" data-formulaire>
<label for="nouvelle-tache">Nouvelle tâche</label>
<input class="atelier-champ" id="nouvelle-tache" name="titre" type="text" required>
<button class="atelier-bouton" type="submit">Ajouter</button>
</form>
<div class="atelier-actions">
<div class="atelier-filtres" role="group" aria-label="Filtrer les tâches">
<button class="atelier-bouton" type="button" data-filtre="toutes" aria-pressed="true">Toutes</button>
<button class="atelier-bouton" type="button" data-filtre="actives" aria-pressed="false">À faire</button>
<button class="atelier-bouton" type="button" data-filtre="terminees" aria-pressed="false">Terminées</button>
</div>
<button class="atelier-bouton" type="button" data-charger>Charger des tâches</button>
</div>
<p class="atelier-message" data-message role="status" aria-live="polite"></p>
<ul class="atelier-liste" data-liste aria-label="Liste des tâches"></ul>
<template data-modele-tache>
<li class="atelier-tache" data-id="">
<input type="checkbox" data-action="basculer">
<span data-titre></span>
<button class="atelier-bouton" type="button" data-action="supprimer">Supprimer</button>
</li>
</template>
</main>
</body>
</html> Résultat attendu [{ id: "tache-1", title: "Préparer la réunion", completed: false }]La clé atelier-taches identifie les données de ce projet. Si aucune donnée n’a encore été enregistrée pour cette clé, getItem() renvoie null. Vous utilisez alors un tableau vide comme état initial : le rendu reste valide, même à la première visite.
Relier la sauvegarde aux actions du gestionnaire Le fichier ci-dessous est un point d’entrée complet pour cette étape. Au chargement, il restaure le tableau avant d’afficher la liste. Chaque action qui modifie les tâches appelle ensuite la même fonction de sauvegarde, ce qui évite d’oublier une opération.
index.html — page complète HTML <!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gestionnaire de tâches</title>
<link rel="stylesheet" href="style.css">
<script type="module" src="js/main.js"></script>
</head>
<body class="atelier-page">
<header class="atelier-entete">
<h1>Gestionnaire de tâches</h1>
<p>Organisez vos tâches et suivez votre progression.</p>
</header>
<main class="atelier-principal">
<section class="atelier-progression" aria-labelledby="titre-progression">
<h2 id="titre-progression">Progression</h2>
<p><span data-progression-texte>0 tâche terminée sur 0</span></p>
<progress class="atelier-progression__barre" data-progression max="100" value="0">0%</progress>
</section>
<form class="atelier-formulaire" data-formulaire>
<label for="nouvelle-tache">Nouvelle tâche</label>
<input class="atelier-champ" id="nouvelle-tache" name="titre" type="text" required>
<button class="atelier-bouton" type="submit">Ajouter</button>
</form>
<div class="atelier-actions">
<div class="atelier-filtres" role="group" aria-label="Filtrer les tâches">
<button class="atelier-bouton" type="button" data-filtre="toutes" aria-pressed="true">Toutes</button>
<button class="atelier-bouton" type="button" data-filtre="actives" aria-pressed="false">À faire</button>
<button class="atelier-bouton" type="button" data-filtre="terminees" aria-pressed="false">Terminées</button>
</div>
<button class="atelier-bouton" type="button" data-charger>Charger des tâches</button>
</div>
<p class="atelier-message" data-message role="status" aria-live="polite"></p>
<ul class="atelier-liste" data-liste aria-label="Liste des tâches"></ul>
<template data-modele-tache>
<li class="atelier-tache" data-id="">
<input type="checkbox" data-action="basculer">
<span data-titre></span>
<button class="atelier-bouton" type="button" data-action="supprimer">Supprimer</button>
</li>
</template>
</main>
</body>
</html> Aperçu du rendu Relancer Arrêter Ouvrir
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gestionnaire de tâches</title>
<link rel="stylesheet" href="style.css">
<script type="module" src="js/main.js"></script>
</head>
<body class="atelier-page">
<header class="atelier-entete">
<h1>Gestionnaire de tâches</h1>
<p>Organisez vos tâches et suivez votre progression.</p>
</header>
<main class="atelier-principal">
<section class="atelier-progression" aria-labelledby="titre-progression">
<h2 id="titre-progression">Progression</h2>
<p><span data-progression-texte>0 tâche terminée sur 0</span></p>
<progress class="atelier-progression__barre" data-progression max="100" value="0">0%</progress>
</section>
<form class="atelier-formulaire" data-formulaire>
<label for="nouvelle-tache">Nouvelle tâche</label>
<input class="atelier-champ" id="nouvelle-tache" name="titre" type="text" required>
<button class="atelier-bouton" type="submit">Ajouter</button>
</form>
<div class="atelier-actions">
<div class="atelier-filtres" role="group" aria-label="Filtrer les tâches">
<button class="atelier-bouton" type="button" data-filtre="toutes" aria-pressed="true">Toutes</button>
<button class="atelier-bouton" type="button" data-filtre="actives" aria-pressed="false">À faire</button>
<button class="atelier-bouton" type="button" data-filtre="terminees" aria-pressed="false">Terminées</button>
</div>
<button class="atelier-bouton" type="button" data-charger>Charger des tâches</button>
</div>
<p class="atelier-message" data-message role="status" aria-live="polite"></p>
<ul class="atelier-liste" data-liste aria-label="Liste des tâches"></ul>
<template data-modele-tache>
<li class="atelier-tache" data-id="">
<input type="checkbox" data-action="basculer">
<span data-titre></span>
<button class="atelier-bouton" type="button" data-action="supprimer">Supprimer</button>
</li>
</template>
</main>
</body>
</html> js/main.js — fichier complet JavaScript const cleStockage = "atelier-taches";
const formulaire = document.querySelector("[data-formulaire]");
const champ = document.querySelector("[name='titre']");
const liste = document.querySelector("[data-liste]");
const modele = document.querySelector("[data-modele-tache]");
// À la première visite, aucune valeur n’existe : l’état commence vide.
const donneesEnregistrees = localStorage.getItem(cleStockage);
let taches = donneesEnregistrees === null
? []
: JSON.parse(donneesEnregistrees);
function enregistrerTaches() {
localStorage.setItem(cleStockage, JSON.stringify(taches));
}
function afficherTaches() {
liste.replaceChildren();
for (const tache of taches) {
const fragment = modele.content.cloneNode(true);
const elementTache = fragment.querySelector(".atelier-tache");
const caseTache = fragment.querySelector("[data-action='basculer']");
elementTache.dataset.id = tache.id;
caseTache.checked = tache.completed;
fragment.querySelector("[data-titre]").textContent = tache.title;
liste.append(fragment);
}
}
function traiterSoumission(evenement) {
evenement.preventDefault();
const donnees = new FormData(formulaire);
const titre = donnees.get("titre").trim();
if (titre === "") return;
taches = [...taches, {
id: crypto.randomUUID(),
title: titre,
completed: false
}];
enregistrerTaches();
afficherTaches();
formulaire.reset();
champ.focus();
}
function traiterAction(evenement) {
const controle = evenement.target.closest("[data-action]");
const elementTache = evenement.target.closest("[data-id]");
if (!controle || !elementTache || !liste.contains(controle)) return;
const id = elementTache.dataset.id;
if (controle.dataset.action === "basculer") {
taches = taches.map((tache) => tache.id === id
? { ...tache, completed: controle.checked }
: tache);
}
if (controle.dataset.action === "supprimer") {
taches = taches.filter((tache) => tache.id !== id);
}
enregistrerTaches();
afficherTaches();
}
formulaire.addEventListener("submit", traiterSoumission);
liste.addEventListener("change", traiterAction);
liste.addEventListener("click", traiterAction);
afficherTaches(); Aperçu du rendu Relancer Arrêter Ouvrir
const cleStockage = "atelier-taches";
const formulaire = document.querySelector("[data-formulaire]");
const champ = document.querySelector("[name='titre']");
const liste = document.querySelector("[data-liste]");
const modele = document.querySelector("[data-modele-tache]");
// À la première visite, aucune valeur n’existe : l’état commence vide.
const donneesEnregistrees = localStorage.getItem(cleStockage);
let taches = donneesEnregistrees === null
? []
: JSON.parse(donneesEnregistrees);
function enregistrerTaches() {
localStorage.setItem(cleStockage, JSON.stringify(taches));
}
function afficherTaches() {
liste.replaceChildren();
for (const tache of taches) {
const fragment = modele.content.cloneNode(true);
const elementTache = fragment.querySelector(".atelier-tache");
const caseTache = fragment.querySelector("[data-action='basculer']");
elementTache.dataset.id = tache.id;
caseTache.checked = tache.completed;
fragment.querySelector("[data-titre]").textContent = tache.title;
liste.append(fragment);
}
}
function traiterSoumission(evenement) {
evenement.preventDefault();
const donnees = new FormData(formulaire);
const titre = donnees.get("titre").trim();
if (titre === "") return;
taches = [...taches, {
id: crypto.randomUUID(),
title: titre,
completed: false
}];
enregistrerTaches();
afficherTaches();
formulaire.reset();
champ.focus();
}
function traiterAction(evenement) {
const controle = evenement.target.closest("[data-action]");
const elementTache = evenement.target.closest("[data-id]");
if (!controle || !elementTache || !liste.contains(controle)) return;
const id = elementTache.dataset.id;
if (controle.dataset.action === "basculer") {
taches = taches.map((tache) => tache.id === id
? { ...tache, completed: controle.checked }
: tache);
}
if (controle.dataset.action === "supprimer") {
taches = taches.filter((tache) => tache.id !== id);
}
enregistrerTaches();
afficherTaches();
}
formulaire.addEventListener("submit", traiterSoumission);
liste.addEventListener("change", traiterAction);
liste.addEventListener("click", traiterAction);
afficherTaches(); <style>:root{--atelier-couleur-texte:#202124;--atelier-couleur-fond:#f5f6fa;--atelier-couleur-accent:#3157c8;--atelier-espace-petit:0.5rem;--atelier-espace-moyen:1rem;--atelier-espace-grand:2rem;--atelier-rayon:0.5rem;--atelier-police:system-ui, sans-serif;--atelier-largeur-page:48rem;--atelier-ombre:0 0.125rem 0.5rem #20212422;}</style><!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gestionnaire de tâches</title>
<link rel="stylesheet" href="style.css">
<script type="module" src="js/main.js"></script>
</head>
<body class="atelier-page">
<header class="atelier-entete">
<h1>Gestionnaire de tâches</h1>
<p>Organisez vos tâches et suivez votre progression.</p>
</header>
<main class="atelier-principal">
<section class="atelier-progression" aria-labelledby="titre-progression">
<h2 id="titre-progression">Progression</h2>
<p><span data-progression-texte>0 tâche terminée sur 0</span></p>
<progress class="atelier-progression__barre" data-progression max="100" value="0">0%</progress>
</section>
<form class="atelier-formulaire" data-formulaire>
<label for="nouvelle-tache">Nouvelle tâche</label>
<input class="atelier-champ" id="nouvelle-tache" name="titre" type="text" required>
<button class="atelier-bouton" type="submit">Ajouter</button>
</form>
<div class="atelier-actions">
<div class="atelier-filtres" role="group" aria-label="Filtrer les tâches">
<button class="atelier-bouton" type="button" data-filtre="toutes" aria-pressed="true">Toutes</button>
<button class="atelier-bouton" type="button" data-filtre="actives" aria-pressed="false">À faire</button>
<button class="atelier-bouton" type="button" data-filtre="terminees" aria-pressed="false">Terminées</button>
</div>
<button class="atelier-bouton" type="button" data-charger>Charger des tâches</button>
</div>
<p class="atelier-message" data-message role="status" aria-live="polite"></p>
<ul class="atelier-liste" data-liste aria-label="Liste des tâches"></ul>
<template data-modele-tache>
<li class="atelier-tache" data-id="">
<input type="checkbox" data-action="basculer">
<span data-titre></span>
<button class="atelier-bouton" type="button" data-action="supprimer">Supprimer</button>
</li>
</template>
</main>
</body>
</html> Après avoir ajouté une tâche, actualisez la page : elle réapparaît, car son état a été sérialisé dans localStorage. Une modification de case ou une suppression est également enregistrée avant le nouveau rendu. Les filtres et le chargement distant ne sont pas traités ici ; ils interviendront dans les leçons prévues à cet effet.
À retenir Utilisez localStorage pour conserver des données côté navigateur entre les actualisations. Convertissez le tableau en chaîne avec JSON.stringify() avant l’enregistrement, puis reconstruisez-le avec JSON.parse() à la lecture. Traitez l’absence de valeur enregistrée comme un état initial vide. Enregistrez après chaque modification de l’état, puis reconstruisez l’affichage depuis cet état. Dans la prochaine leçon, vous vérifierez la structure et le contenu des données restaurées avant de les utiliser.