Files
Portfolio-Codex/docs/stories/5.2.validation-javascript.md

352 lines
10 KiB
Markdown

# Story 5.2: Validation JavaScript Côté Client
## Status
Ready for Dev
## Story
**As a** visiteur,
**I want** être informé immédiatement si je fais une erreur de saisie,
**so that** je corrige avant d'envoyer et j'évite les allers-retours.
## Acceptance Criteria
1. La validation JavaScript s'exécute à la soumission ET à la perte de focus (blur)
2. Les messages d'erreur sont affichés sous chaque champ concerné
3. Les champs en erreur sont visuellement distingués (bordure rouge, icône)
4. Le message d'erreur est clair et indique comment corriger
5. Le bouton d'envoi est désactivé tant que le formulaire contient des erreurs
6. La validation est en JavaScript vanilla (pas de bibliothèque)
## Tasks / Subtasks
- [] **Task 1 : Créer le validateur de formulaire** (AC: 6)
- [] Créer `assets/js/contact-form.js`
- [] Classe ou objet `FormValidator`
- [] Méthodes de validation par type de champ
- [] **Task 2 : Implémenter la validation au blur** (AC: 1)
- [] Écouter l'événement `blur` sur chaque champ
- [] Valider le champ concerné
- [] Afficher/masquer l'erreur
- [] **Task 3 : Implémenter la validation à la soumission** (AC: 1)
- [] Écouter l'événement `submit`
- [] Valider tous les champs
- [] Empêcher l'envoi si erreurs
- [] **Task 4 : Afficher les erreurs** (AC: 2, 3, 4)
- [] Message sous le champ (data-error)
- [] Bordure rouge sur le champ (classes Tailwind)
- [] Messages clairs et actionnables
- [] **Task 5 : Gérer l'état du bouton** (AC: 5)
- [] Désactiver si erreurs
- [] Réactiver quand tout est valide
## Dev Notes
### Structure assets/js/contact-form.js
```javascript
/**
* Validation du formulaire de contact
* JavaScript vanilla - pas de dépendances
*/
class FormValidator {
constructor(formId) {
this.form = document.getElementById(formId);
if (!this.form) return;
this.submitBtn = document.getElementById('submit-btn');
this.fields = {};
this.errors = {};
this.init();
}
init() {
// Définir les règles de validation
this.rules = {
nom: {
required: true,
minLength: 2,
maxLength: 100,
message: 'Veuillez entrer votre nom (2 caractères minimum)'
},
prenom: {
required: true,
minLength: 2,
maxLength: 100,
message: 'Veuillez entrer votre prénom (2 caractères minimum)'
},
email: {
required: true,
email: true,
message: 'Veuillez entrer une adresse email valide'
},
categorie: {
required: true,
message: 'Veuillez sélectionner une catégorie'
},
objet: {
required: true,
minLength: 5,
maxLength: 200,
message: 'Veuillez entrer un objet (5 caractères minimum)'
},
message: {
required: true,
minLength: 20,
maxLength: 5000,
message: 'Veuillez entrer votre message (20 caractères minimum)'
}
};
// Récupérer les champs
Object.keys(this.rules).forEach(fieldName => {
this.fields[fieldName] = this.form.querySelector(`[name="${fieldName}"]`);
});
this.bindEvents();
}
bindEvents() {
// Validation au blur
Object.keys(this.fields).forEach(fieldName => {
const field = this.fields[fieldName];
if (field) {
field.addEventListener('blur', () => this.validateField(fieldName));
field.addEventListener('input', () => this.clearError(fieldName));
}
});
// Validation à la soumission
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
// Compteur de caractères pour le message
const messageField = this.fields.message;
if (messageField) {
messageField.addEventListener('input', () => this.updateCharCount());
}
}
validateField(fieldName) {
const field = this.fields[fieldName];
const rule = this.rules[fieldName];
if (!field || !rule) return true;
const value = field.value.trim();
let isValid = true;
let errorMessage = '';
// Required
if (rule.required && !value) {
isValid = false;
errorMessage = rule.message;
}
// Min length
if (isValid && rule.minLength && value.length < rule.minLength) {
isValid = false;
errorMessage = rule.message;
}
// Max length
if (isValid && rule.maxLength && value.length > rule.maxLength) {
isValid = false;
errorMessage = `Maximum ${rule.maxLength} caractères`;
}
// Email format
if (isValid && rule.email && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
isValid = false;
errorMessage = rule.message;
}
}
// Afficher ou masquer l'erreur
if (isValid) {
this.clearError(fieldName);
} else {
this.showError(fieldName, errorMessage);
}
this.errors[fieldName] = !isValid;
this.updateSubmitButton();
return isValid;
}
validateAll() {
let allValid = true;
Object.keys(this.rules).forEach(fieldName => {
if (!this.validateField(fieldName)) {
allValid = false;
}
});
return allValid;
}
showError(fieldName, message) {
const field = this.fields[fieldName];
const errorEl = this.form.querySelector(`[data-error="${fieldName}"]`);
if (field) {
field.classList.add('input-error');
field.setAttribute('aria-invalid', 'true');
}
if (errorEl) {
errorEl.textContent = message;
errorEl.classList.remove('hidden');
}
}
clearError(fieldName) {
const field = this.fields[fieldName];
const errorEl = this.form.querySelector(`[data-error="${fieldName}"]`);
if (field) {
field.classList.remove('input-error');
field.removeAttribute('aria-invalid');
}
if (errorEl) {
errorEl.textContent = '';
errorEl.classList.add('hidden');
}
this.errors[fieldName] = false;
this.updateSubmitButton();
}
updateSubmitButton() {
const hasErrors = Object.values(this.errors).some(err => err);
if (this.submitBtn) {
this.submitBtn.disabled = hasErrors;
}
}
updateCharCount() {
const messageField = this.fields.message;
const countEl = document.getElementById('message-count');
if (messageField && countEl) {
countEl.textContent = messageField.value.length;
}
}
handleSubmit(e) {
e.preventDefault();
if (!this.validateAll()) {
// Focus sur le premier champ en erreur
const firstError = Object.keys(this.errors).find(key => this.errors[key]);
if (firstError && this.fields[firstError]) {
this.fields[firstError].focus();
}
return;
}
// Si valide, déclencher l'envoi (géré par une autre partie du code)
this.form.dispatchEvent(new CustomEvent('validSubmit'));
}
getFormData() {
const formData = {};
Object.keys(this.fields).forEach(fieldName => {
if (this.fields[fieldName]) {
formData[fieldName] = this.fields[fieldName].value.trim();
}
});
// Ajouter le champ entreprise (optionnel)
const entreprise = this.form.querySelector('[name="entreprise"]');
if (entreprise) {
formData.entreprise = entreprise.value.trim();
}
return formData;
}
}
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
window.contactFormValidator = new FormValidator('contact-form');
});
```
### Messages d'Erreur
| Champ | Message |
|-------|---------|
| nom | Veuillez entrer votre nom (2 caractères minimum) |
| prenom | Veuillez entrer votre prénom (2 caractères minimum) |
| email | Veuillez entrer une adresse email valide |
| categorie | Veuillez sélectionner une catégorie |
| objet | Veuillez entrer un objet (5 caractères minimum) |
| message | Veuillez entrer votre message (20 caractères minimum) |
### Règles de Validation
| Champ | Required | Min | Max | Format |
|-------|----------|-----|-----|--------|
| nom | Oui | 2 | 100 | - |
| prenom | Oui | 2 | 100 | - |
| email | Oui | - | 255 | email |
| entreprise | Non | - | 200 | - |
| categorie | Oui | - | - | - |
| objet | Oui | 5 | 200 | - |
| message | Oui | 20 | 5000 | - |
## Testing
- [] La validation se déclenche au blur
- [] La validation se déclenche à la soumission
- [] Les messages d'erreur s'affichent sous les champs
- [] Les champs en erreur ont une bordure rouge
- [] Le bouton est désactivé si erreurs
- [] Le compteur de caractères fonctionne
- [] Le focus va au premier champ en erreur
- [] Email invalide est détecté
## Dev Agent Record
### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
### File List
| File | Action | Description |
|------|--------|-------------|
| `assets/js/contact-form.js` | Created | Classe FormValidator avec validation complète |
| `pages/contact.php` | Modified | Lien vers le script JS, classes Tailwind pour erreurs |
### Completion Notes
- Classe FormValidator en JavaScript vanilla (pas de dépendances)
- Validation au blur et à la soumission
- Messages d'erreur sous chaque champ avec data-error
- Bordure rouge sur les champs invalides (classes Tailwind)
- Bouton submit désactivé si erreurs (updateSubmitButton)
- Compteur de caractères en temps réel
- Focus automatique sur le premier champ en erreur
- Validation email avec regex
- Événement 'validSubmit' dispatché quand tout est valide
- Gestion du reset du formulaire
### 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) |