Ce que vous allez construire
Une application de to-do list complète avec ajout, suppression, modification et persistance des données dans le localStorage. Ce projet couvre les concepts fondamentaux de JavaScript : manipulation du DOM, gestion des événements, localStorage, et structuration du code. Vous pouvez tester le résultat final directement dans votre navigateur.
Le HTML
<div class="todo-app">
<h1>Mes tâches</h1>
<form id="todoForm" class="todo-form">
<input type="text" id="todoInput" placeholder="Ajouter une tâche..." required>
<button type="submit">Ajouter</button>
</form>
<div class="todo-filters">
<button class="filter-btn active" data-filter="all">Toutes</button>
<button class="filter-btn" data-filter="active">Actives</button>
<button class="filter-btn" data-filter="completed">Terminées</button>
</div>
<ul id="todoList" class="todo-list"></ul>
<p class="todo-count"><span id="todoCount">0</span> tâche(s) restante(s)</p>
</div>
Le CSS
.todo-app {
max-width: 500px;
margin: 40px auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.todo-form {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.todo-form input {
flex: 1;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
}
.todo-form input:focus {
outline: none;
border-color: #4a90d9;
}
.todo-form button {
padding: 12px 24px;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
transition: opacity 0.3s;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-text {
flex: 1;
font-size: 16px;
}
.todo-delete {
background: none;
border: none;
color: #e74c3c;
cursor: pointer;
font-size: 18px;
opacity: 0;
transition: opacity 0.2s;
}
.todo-item:hover .todo-delete {
opacity: 1;
}
.filter-btn {
padding: 6px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 20px;
cursor: pointer;
margin-right: 8px;
}
.filter-btn.active {
background: #4a90d9;
color: white;
border-color: #4a90d9;
}
Le JavaScript complet
// État de l'application
let todos = JSON.parse(localStorage.getItem('todos')) || [];
let currentFilter = 'all';
// Éléments du DOM
const form = document.getElementById('todoForm');
const input = document.getElementById('todoInput');
const list = document.getElementById('todoList');
const countEl = document.getElementById('todoCount');
const filterBtns = document.querySelectorAll('.filter-btn');
// Générer un ID unique
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// Sauvegarder dans localStorage
function save() {
localStorage.setItem('todos', JSON.stringify(todos));
}
// Afficher les tâches
function render() {
const filtered = todos.filter(todo => {
if (currentFilter === 'active') return !todo.completed;
if (currentFilter === 'completed') return todo.completed;
return true;
});
list.innerHTML = filtered.map(todo =>
'<li class="todo-item ' + (todo.completed ? 'completed' : '') + '" data-id="' + todo.id + '">' +
'<input type="checkbox" ' + (todo.completed ? 'checked' : '') + '>' +
'<span class="todo-text">' + escapeHtml(todo.text) + '</span>' +
'<button class="todo-delete">✕</button>' +
'</li>'
).join('');
// Compter les tâches actives
const activeCount = todos.filter(t => !t.completed).length;
countEl.textContent = activeCount;
}
// Échapper le HTML pour éviter les XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Ajouter une tâche
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
todos.push({ id: generateId(), text, completed: false });
save();
render();
input.value = '';
input.focus();
});
// Cocher/décocher et supprimer (délégation d'événements)
list.addEventListener('click', (e) => {
const item = e.target.closest('.todo-item');
if (!item) return;
const id = item.dataset.id;
if (e.target.type === 'checkbox') {
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
save();
render();
}
if (e.target.classList.contains('todo-delete')) {
todos = todos.filter(t => t.id !== id);
save();
render();
}
});
// Double-clic pour éditer
list.addEventListener('dblclick', (e) => {
if (!e.target.classList.contains('todo-text')) return;
const item = e.target.closest('.todo-item');
const id = item.dataset.id;
const todo = todos.find(t => t.id === id);
const editInput = document.createElement('input');
editInput.type = 'text';
editInput.value = todo.text;
editInput.className = 'todo-edit';
e.target.replaceWith(editInput);
editInput.focus();
function finishEdit() {
const newText = editInput.value.trim();
if (newText) {
todo.text = newText;
save();
}
render();
}
editInput.addEventListener('blur', finishEdit);
editInput.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') finishEdit();
if (ev.key === 'Escape') render(); // Annuler
});
});
// Filtres
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
render();
});
});
// Affichage initial
render();
Concepts clés dans ce projet
- localStorage : les données persistent entre les sessions du navigateur.
JSON.stringifypour sauvegarder,JSON.parsepour lire. - Délégation d’événements : un seul écouteur sur la liste parente gère les clics sur tous les éléments enfants, même ceux ajoutés dynamiquement.
- Échappement HTML : la fonction
escapeHtml()empêche les attaques XSS si un utilisateur tape du HTML dans le champ. - État centralisé : toute l’application est pilotée par le tableau
todos. On modifie l’état, on sauvegarde, on re-render.
Exercice d’extension
- Ajoutez un bouton « Supprimer les tâches terminées »
- Ajoutez le drag & drop pour réordonner les tâches
- Ajoutez des dates d’échéance avec un code couleur (rouge si en retard)
- Ajoutez des catégories (Travail, Personnel, Urgent)