8.4 KiB
8.4 KiB
Story 5.3: Persistance des Données avec localStorage
Status
Ready for Dev
Story
As a visiteur, I want que mes données soient sauvegardées si je quitte la page, so that je ne ressaisisse pas tout si je reviens plus tard.
Acceptance Criteria
- Chaque modification d'un champ sauvegarde automatiquement dans localStorage
- Au chargement de la page, les champs sont pré-remplis avec les données sauvegardées
- Le localStorage est vidé après un envoi réussi du formulaire
- Un bouton "Effacer le formulaire" permet de réinitialiser manuellement
- Les données sensibles (si ajoutées plus tard) ne sont PAS stockées
- Le stockage utilise une clé unique (ex:
portfolio_contact_form)
Tasks / Subtasks
-
[] Task 1 : Créer le gestionnaire de stockage (AC: 6)
- [] Clé unique
portfolio_contact_form - [] Méthodes save, load, clear
- [] Gestion des erreurs (localStorage indisponible)
- [] Clé unique
-
[] Task 2 : Sauvegarder automatiquement (AC: 1)
- [] Écouter l'événement
inputsur chaque champ - [] Debounce pour éviter trop d'écritures (500ms)
- [] Sauvegarder l'état complet
- [] Écouter l'événement
-
[] Task 3 : Restaurer au chargement (AC: 2)
- [] Charger les données au DOMContentLoaded
- [] Pré-remplir chaque champ
- [] Mettre à jour le compteur de caractères
-
[] Task 4 : Vider après envoi réussi (AC: 3)
- [] Appeler clear() après succès (événement formSuccess)
- [] Réinitialiser le formulaire
-
[] Task 5 : Bouton "Effacer" (AC: 4)
- [] Écouter le clic sur le bouton (id="clear-form-btn")
- [] Vider le localStorage
- [] Réinitialiser le formulaire
- [] Confirmation avec confirm()
-
[] Task 6 : Exclure les données sensibles (AC: 5)
- [] Ne pas stocker csrf_token, password, recaptcha_token
- [] Documenté dans EXCLUDED_FIELDS
Dev Notes
Gestionnaire de Stockage (state.js)
// assets/js/state.js
/**
* Gestionnaire d'état pour le localStorage
*/
const AppState = {
STORAGE_KEY: 'portfolio_contact_form',
// Champs à ne jamais stocker
EXCLUDED_FIELDS: ['csrf_token', 'password', 'recaptcha_token'],
/**
* Vérifie si localStorage est disponible
*/
isStorageAvailable() {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
},
/**
* Sauvegarde les données du formulaire
*/
saveFormData(data) {
if (!this.isStorageAvailable()) return;
try {
// Filtrer les champs exclus
const filteredData = {};
Object.keys(data).forEach(key => {
if (!this.EXCLUDED_FIELDS.includes(key)) {
filteredData[key] = data[key];
}
});
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredData));
} catch (e) {
console.warn('Impossible de sauvegarder dans localStorage:', e);
}
},
/**
* Charge les données sauvegardées
*/
getFormData() {
if (!this.isStorageAvailable()) return null;
try {
const data = localStorage.getItem(this.STORAGE_KEY);
return data ? JSON.parse(data) : null;
} catch (e) {
console.warn('Impossible de charger depuis localStorage:', e);
return null;
}
},
/**
* Efface les données sauvegardées
*/
clearFormData() {
if (!this.isStorageAvailable()) return;
try {
localStorage.removeItem(this.STORAGE_KEY);
} catch (e) {
// Silencieux
}
}
};
Intégration dans contact-form.js
// Ajouter dans la classe FormValidator ou en complément
class ContactFormPersistence {
constructor(formId) {
this.form = document.getElementById(formId);
if (!this.form) return;
this.debounceTimer = null;
this.init();
}
init() {
this.loadSavedData();
this.bindEvents();
}
bindEvents() {
// Sauvegarder à chaque modification (avec debounce)
this.form.addEventListener('input', () => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => this.saveData(), 500);
});
// Bouton effacer
const clearBtn = document.getElementById('clear-form-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => this.clearForm());
}
// Écouter l'envoi réussi
this.form.addEventListener('formSuccess', () => {
AppState.clearFormData();
});
}
saveData() {
const formData = new FormData(this.form);
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});
AppState.saveFormData(data);
}
loadSavedData() {
const savedData = AppState.getFormData();
if (!savedData) return;
Object.keys(savedData).forEach(key => {
const field = this.form.querySelector(`[name="${key}"]`);
if (field && savedData[key]) {
field.value = savedData[key];
}
});
// Mettre à jour le compteur de caractères
const messageField = this.form.querySelector('[name="message"]');
const countEl = document.getElementById('message-count');
if (messageField && countEl) {
countEl.textContent = messageField.value.length;
}
}
clearForm() {
// Confirmation optionnelle
if (!confirm('Êtes-vous sûr de vouloir effacer le formulaire ?')) {
return;
}
// Vider le localStorage
AppState.clearFormData();
// Réinitialiser le formulaire
this.form.reset();
// Réinitialiser le compteur
const countEl = document.getElementById('message-count');
if (countEl) {
countEl.textContent = '0';
}
// Effacer les erreurs visuelles
this.form.querySelectorAll('.input-error').forEach(el => {
el.classList.remove('input-error');
});
this.form.querySelectorAll('[data-error]').forEach(el => {
el.classList.add('hidden');
el.textContent = '';
});
}
}
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
window.contactFormPersistence = new ContactFormPersistence('contact-form');
});
Clé de Stockage
localStorage key: "portfolio_contact_form"
Exemple de données stockées:
{
"nom": "Dupont",
"prenom": "Marie",
"email": "marie@example.com",
"entreprise": "Acme Corp",
"categorie": "projet",
"objet": "Nouveau site web",
"message": "Bonjour, je souhaite..."
}
Champs Exclus du Stockage
csrf_token(sécurité)password(si ajouté)recaptcha_token(généré dynamiquement)
Testing
- [] Les données sont sauvegardées à la saisie
- [] Les données sont restaurées au rechargement
- [] Le bouton "Effacer" vide le formulaire
- [] Le localStorage est vidé après envoi réussi (événement formSuccess)
- [] Les champs exclus ne sont pas stockés (EXCLUDED_FIELDS)
- [] Pas d'erreur si localStorage indisponible (isStorageAvailable)
- [] Le compteur de caractères est mis à jour au chargement
Dev Agent Record
Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
File List
| File | Action | Description |
|---|---|---|
assets/js/state.js |
Created | Objet AppState pour gestion localStorage |
assets/js/contact-form.js |
Modified | Ajout classe ContactFormPersistence |
pages/contact.php |
Modified | Bouton Effacer + script state.js |
Completion Notes
- AppState avec clé unique
portfolio_contact_form - Vérification disponibilité localStorage (try/catch)
- Filtrage des champs sensibles (csrf_token, password, recaptcha_token)
- Debounce de 500ms sur la sauvegarde
- Restauration automatique au chargement de la page
- Bouton "Effacer" avec confirmation et reset complet
- Événement
formSuccesspour vider après envoi réussi - Scripts chargés avec defer pour ne pas bloquer le rendu
Debug Log References
Aucun problème rencontré.
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
| 2026-01-24 | 1.0 | Implémentation complète | James (Dev) |