Assembler la structure et le rendu
Objectif : Créer une page de gestion des tâches qui affiche l’état initial et met à jour la liste après chaque modification.
Jusqu’ici, les tâches chargées depuis le réseau ne rejoignaient pas l’état du gestionnaire : le résultat de la requête était seulement annoncé. Vous allez réunir le formulaire, les actions sur les tâches, les filtres, le stockage et le chargement distant dans un même point d’entrée. La liste sera toujours reconstruite depuis l’état, ce qui évite de laisser l’affichage diverger des données.
Un état, plusieurs affichages
L’état applicatif est la source de vérité : les fonctions de commande le modifient, puis une fonction de rendu actualise la liste à partir de son contenu. Le filtre actif ne supprime aucune tâche ; il choisit seulement lesquelles afficher. La restauration du stockage au démarrage fournit l’état initial, et chaque modification locale le sauvegarde ensuite.
Dans cet assemblage, les tâches distantes proviennent de l’API JSONPlaceholder. Leurs identifiants sont convertis en chaînes et préfixés par distante- pour les distinguer des identifiants locaux. Les tâches reçues sont ajoutées à l’état existant sans réintroduire une tâche distante déjà présente.
Assembler le point d’entrée
Conservez le index.html et le style.css du contrat. Remplacez le contenu de js/main.js par le fichier complet ci-dessous : il utilise les sélecteurs et le modèle HTML déjà présents dans la page. La progression reste à sa valeur actuelle ; son calcul et sa synchronisation feront l’objet de la prochaine leçon.
const cleStockage = "atelier-taches";
const versionStockage = 1;
const formulaire = document.querySelector("[data-formulaire]");
const champTitre = document.querySelector("[name='titre']");
const liste = document.querySelector("[data-liste]");
const modele = document.querySelector("[data-modele-tache]");
const message = document.querySelector("[data-message]");
const boutonCharger = document.querySelector("[data-charger]");
const boutonsFiltres = document.querySelectorAll("[data-filtre]");
if (!formulaire || !champTitre || !liste || !modele || !message || !boutonCharger || boutonsFiltres.length === 0) {
throw new Error("Un élément nécessaire du gestionnaire est absent de la page.");
}
let taches = restaurerTaches();
let filtreActif = "toutes";
function validerTaches(valeur) {
if (!Array.isArray(valeur)) {
return [];
}
return valeur.filter(function (tache) {
return tache !== null
&& typeof tache === "object"
&& typeof tache.id === "string"
&& typeof tache.titre === "string"
&& typeof tache.terminee === "boolean";
});
}
function restaurerTaches() {
try {
const texteEnregistre = localStorage.getItem(cleStockage);
if (texteEnregistre === null) {
return [];
}
const donnees = JSON.parse(texteEnregistre);
if (Array.isArray(donnees)) {
return validerTaches(donnees);
}
if (donnees && donnees.version === versionStockage && Array.isArray(donnees.taches)) {
return validerTaches(donnees.taches);
}
return [];
} catch (erreur) {
return [];
}
}
function enregistrerTaches() {
try {
localStorage.setItem(cleStockage, JSON.stringify({
version: versionStockage,
taches: taches
}));
} catch (erreur) {
message.textContent = "Impossible d’enregistrer les tâches dans ce navigateur.";
}
}
function creerElementTache(tache) {
const fragment = modele.content.cloneNode(true);
const element = fragment.querySelector(".atelier-tache");
const caseTache = fragment.querySelector("[data-action='basculer']");
const titre = fragment.querySelector("[data-titre]");
element.dataset.id = tache.id;
caseTache.checked = tache.terminee;
titre.textContent = tache.titre;
return fragment;
}
function obtenirTachesAffichees() {
if (filtreActif === "actives") {
return taches.filter(function (tache) {
return !tache.terminee;
});
}
if (filtreActif === "terminees") {
return taches.filter(function (tache) {
return tache.terminee;
});
}
return taches;
}
function rendreListe() {
liste.replaceChildren();
obtenirTachesAffichees().forEach(function (tache) {
liste.append(creerElementTache(tache));
});
}
function publierChangement() {
enregistrerTaches();
rendreListe();
}
function traiterSoumission(evenement) {
evenement.preventDefault();
const donnees = new FormData(formulaire);
const titre = String(donnees.get("titre")).trim();
if (titre === "") {
message.textContent = "Saisissez un titre pour ajouter une tâche.";
return;
}
taches = taches.concat({
id: crypto.randomUUID(),
titre: titre,
terminee: false
});
formulaire.reset();
message.textContent = "Tâche ajoutée.";
publierChangement();
}
function traiterActionListe(evenement) {
const cible = evenement.target;
if (!(cible instanceof Element)) {
return;
}
const commande = cible.closest("[data-action]");
const elementTache = cible.closest(".atelier-tache");
if (!commande || !elementTache || !liste.contains(elementTache)) {
return;
}
const id = elementTache.dataset.id;
if (commande.dataset.action === "basculer") {
taches = taches.map(function (tache) {
return tache.id === id
? { ...tache, terminee: commande.checked }
: tache;
});
message.textContent = "État de la tâche mis à jour.";
publierChangement();
return;
}
if (commande.dataset.action === "supprimer") {
taches = taches.filter(function (tache) {
return tache.id !== id;
});
message.textContent = "Tâche supprimée.";
publierChangement();
}
}
function traiterFiltre(evenement) {
const cible = evenement.target;
if (!(cible instanceof Element)) {
return;
}
const bouton = cible.closest("[data-filtre]");
if (!bouton) {
return;
}
filtreActif = bouton.dataset.filtre;
boutonsFiltres.forEach(function (element) {
element.setAttribute("aria-pressed", String(element === bouton));
});
rendreListe();
}
async function chargerTachesDistantes() {
boutonCharger.disabled = true;
message.textContent = "Chargement des tâches…";
try {
const reponse = await fetch("https://jsonplaceholder.typicode.com/todos?_limit=5");
if (!reponse.ok) {
throw new Error("La réponse HTTP indique une erreur : " + reponse.status);
}
const donnees = await reponse.json();
const tachesDistantes = donnees
.filter(function (tache) {
return tache && typeof tache.id === "number" && typeof tache.title === "string" && typeof tache.completed === "boolean";
})
.map(function (tache) {
return {
id: "distante-" + tache.id,
titre: tache.title,
terminee: tache.completed
};
});
const identifiantsExistants = new Set(taches.map(function (tache) {
return tache.id;
}));
const nouvellesTaches = tachesDistantes.filter(function (tache) {
return !identifiantsExistants.has(tache.id);
});
taches = taches.concat(nouvellesTaches);
publierChangement();
message.textContent = nouvellesTaches.length > 0
? nouvellesTaches.length + " tâche(s) chargée(s)."
: "Aucune nouvelle tâche à ajouter.";
} catch (erreur) {
message.textContent = "Le chargement a échoué. Vérifiez votre connexion puis réessayez.";
} finally {
boutonCharger.disabled = false;
}
}
formulaire.addEventListener("submit", traiterSoumission);
liste.addEventListener("click", traiterActionListe);
liste.addEventListener("change", traiterActionListe);
document.querySelector("[data-filtres]");
const groupeFiltres = document.querySelector(".atelier-filtres");
if (groupeFiltres) {
groupeFiltres.addEventListener("click", traiterFiltre);
}
boutonCharger.addEventListener("click", chargerTachesDistantes);
rendreListe();Au chargement de la page, restaurerTaches() fournit le tableau initial, puis rendreListe() affiche immédiatement les tâches selon le filtre « Toutes ». Une soumission ajoute une tâche à l’état, enregistre celui-ci et reconstruit la liste. Les commandes de chaque ligne suivent le même cycle : modifier l’état, persister, puis rendre à nouveau ; changer de filtre ne modifie que la sélection affichée.
Le bouton de chargement réutilise le traitement asynchrone des leçons précédentes, puis ajoute au tableau uniquement les identifiants distants absents. L’appel à publierChangement() rend les données reçues visibles et les inclut dans la sauvegarde. Le message de statut indique si des tâches nouvelles ont été ajoutées ou si le chargement n’a fourni aucun nouvel élément.
À retenir
- L’état des tâches est la source de vérité ; la liste est reconstruite à partir de cet état.
- Une modification locale suit le même ordre : mettre à jour l’état, enregistrer, puis rendre.
- Les filtres changent les tâches visibles sans modifier les tâches conservées.
- Les données distantes sont converties au format du projet et ajoutées sans doublons.
Dans la prochaine leçon, vous calculerez la progression depuis l’état et synchroniserez sa valeur avec les éléments déjà présents dans l’interface.