Une application de prise de notes est un excellent projet pour apprendre JavaScript : on y retrouve tout ce qui fait une vraie application, c'est-à-dire un formulaire, une liste générée par le code, la modification, la suppression, une recherche et une sauvegarde dans le navigateur. Tu vas la construire en HTML, CSS et JavaScript, sans bibliothèque. Durée : environ 1 heure.
Ce que tu vas construire
- un formulaire pour écrire une note (titre et texte) ;
- la liste des notes, la plus récente en haut, avec sa date de modification ;
- des boutons pour modifier ou supprimer chaque note ;
- une recherche instantanée dans les titres et les textes ;
- une sauvegarde automatique : les notes sont toujours là quand tu reviens.
Crée un dossier avec index.html, style.css et script.js.
1. Le HTML : le formulaire, la liste et un modèle
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mes notes</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="app">
<h1>Mes notes</h1>
<form id="note-form" class="card">
<input type="hidden" name="id">
<label for="note-title">Titre</label>
<input id="note-title" name="title" required maxlength="80">
<label for="note-text">Note</label>
<textarea id="note-text" name="text" rows="4"></textarea>
<div class="actions">
<button type="submit">Enregistrer</button>
<button type="reset" class="secondary">Annuler</button>
</div>
</form>
<label for="search">Rechercher</label>
<input id="search" type="search">
<ul id="notes" class="notes"></ul>
<p id="empty" class="empty" hidden></p>
</main>
<!-- Modèle d'une note, copié par le JavaScript. -->
<template id="note-template">
<li class="card">
<h2 class="note-title"></h2>
<p class="note-text"></p>
<p class="note-date"></p>
<div class="actions">
<button type="button" data-action="edit" class="secondary">Modifier</button>
<button type="button" data-action="delete" class="secondary">Supprimer</button>
</div>
</li>
</template>
<script src="script.js"></script>
</body>
</html>
Deux détails :
- le champ caché
idindique au code si on crée une note (vide) ou si on en modifie une (l'identifiant de la note) ; - la balise
<template>contient le modèle d'une note. Le navigateur ne l'affiche pas : le JavaScript le copie pour chaque note. Le HTML reste lisible dans le fichier HTML, au lieu d'être construit morceau par morceau dans le code.
2. Le CSS
body {
margin: 0;
background: #f6f4ef;
font-family: system-ui, sans-serif;
line-height: 1.5;
}
.app {
max-width: 40rem;
margin: 0 auto;
padding: 2rem 1rem;
}
label {
display: block;
margin-top: 0.75rem;
font-weight: 600;
}
input,
textarea {
box-sizing: border-box;
width: 100%;
padding: 0.5rem;
border: 1px solid #bbb;
border-radius: 6px;
font: inherit;
}
.card {
margin-bottom: 1rem;
padding: 1rem;
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 4px rgb(0 0 0 / 0.08);
}
.notes {
margin: 1.5rem 0 0;
padding: 0;
list-style: none;
}
.note-title {
margin: 0;
font-size: 1.15rem;
}
/* Garde les retours à la ligne saisis dans la note. */
.note-text {
white-space: pre-wrap;
}
.note-date,
.empty {
color: #666;
font-size: 0.875rem;
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
button {
padding: 0.5rem 1rem;
border: 0;
border-radius: 6px;
background: #25737d;
color: #fff;
font: inherit;
cursor: pointer;
}
button.secondary {
background: #e6ecec;
color: #1d2b2d;
}
Retiens white-space: pre-wrap sur le texte de la note : sans lui, les retours à la ligne saisis dans le champ disparaîtraient à l'affichage.
3. Le JavaScript
const STORAGE_KEY = 'mes-notes';
const form = document.querySelector('#note-form');
const list = document.querySelector('#notes');
const empty = document.querySelector('#empty');
const search = document.querySelector('#search');
const template = document.querySelector('#note-template');
let notes = load();
function load() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? [];
} catch {
return []; // Données illisibles : on repart de zéro plutôt que de planter.
}
}
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
}
function render() {
const query = search.value.trim().toLowerCase();
const visible = notes.filter((note) =>
(note.title + ' ' + note.text).toLowerCase().includes(query));
list.replaceChildren(...visible.map((note) => {
const item = template.content.firstElementChild.cloneNode(true);
const date = new Date(note.updatedAt).toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' });
item.dataset.id = note.id;
// textContent, jamais innerHTML : le texte saisi est affiché tel quel, sans être interprété.
item.querySelector('.note-title').textContent = note.title;
item.querySelector('.note-text').textContent = note.text;
item.querySelector('.note-date').textContent = 'Modifiée le ' + date;
return item;
}));
empty.hidden = visible.length > 0;
empty.textContent = notes.length === 0
? 'Aucune note pour l\'instant : écris la première !'
: 'Aucune note ne correspond à ta recherche.';
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const data = new FormData(form);
const id = data.get('id');
const note = {
title: data.get('title').trim(),
text: data.get('text').trim(),
updatedAt: Date.now(),
};
if (id) {
// Modification : on remplace la note qui a cet identifiant.
notes = notes.map((existing) => (existing.id === id ? { ...existing, ...note } : existing));
} else {
// Nouvelle note : en haut de la liste.
notes.unshift({ id: Date.now().toString(36), ...note });
}
save();
form.reset();
render();
});
// Un champ caché garde sa valeur au reset : on le vide nous-mêmes.
form.addEventListener('reset', () => {
form.elements.id.value = '';
});
// Un seul écouteur pour tous les boutons de la liste (délégation d'évènements).
list.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
if (!button) {
return;
}
const id = button.closest('li').dataset.id;
const note = notes.find((candidate) => candidate.id === id);
if (button.dataset.action === 'edit') {
form.elements.id.value = note.id;
form.elements.title.value = note.title;
form.elements.text.value = note.text;
form.elements.title.focus();
} else if (confirm(`Supprimer la note « ${note.title} » ?`)) {
notes = notes.filter((candidate) => candidate.id !== id);
save();
render();
}
});
search.addEventListener('input', render);
render();
Le code suit un principe simple, qu'on retrouve dans React ou Vue : les données d'abord, l'affichage ensuite. Le tableau notes est la seule source de vérité. Chaque action le modifie, l'enregistre avec save(), puis redessine la liste avec render(). On ne modifie jamais l'affichage à la main.
localStoragene stocke que du texte :JSON.stringifytransforme le tableau en texte,JSON.parsefait l'inverse. Letry/catchévite que l'application plante si les données ont été abîmées.textContent, jamaisinnerHTMLpour afficher ce que l'utilisateur a saisi : si quelqu'un écrit<img src=x onerror=…>dans une note, le texte s'affiche tel quel au lieu d'exécuter du code. C'est la protection de base contre les failles XSS.FormDatalit tous les champs du formulaire d'un coup, par leur attributname.- La délégation d'évènements : au lieu d'un écouteur par bouton, un seul écouteur sur la liste.
event.target.closest('button[data-action]')retrouve le bouton cliqué, même pour les notes ajoutées après le chargement. - Le piège du champ caché : un
input type="hidden"garde sa valeur quand on réinitialise le formulaire. Sans l'écouteurresetqui le vide, la note suivante écraserait la précédente.
4. Tester
- Crée deux notes, puis recharge la page : elles sont toujours là.
- Modifie une note : elle est mise à jour, sans doublon.
- Tape un mot dans la recherche : seules les notes qui le contiennent restent affichées.
- Écris du HTML dans une note : il s'affiche comme du texte.
- Dans les outils de développement, onglet Application → Local Storage : tu vois tes notes au format JSON.
Pour aller plus loin
- Tout comprendre de
localStorage: limites, sécurité et alternatives dans le tutoriel sauvegarder des données localement. - Des notes épinglées : ajoute une propriété
pinnedet trie le tableau pour les garder en haut. - Exporter ses notes : un bouton qui télécharge le JSON, avec
BlobetURL.createObjectURL. - Synchroniser entre appareils : remplace
localStoragepar une petite API en PHP et une base MySQL, comme dans le tutoriel blog en PHP et MySQL.
Ton application est en ligne ? Partage-la dans la vitrine des lecteurs : les abonnés votent chaque mois pour leur projet préféré.