10 KiB
10 KiB
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
- La validation JavaScript s'exécute à la soumission ET à la perte de focus (blur)
- Les messages d'erreur sont affichés sous chaque champ concerné
- Les champs en erreur sont visuellement distingués (bordure rouge, icône)
- Le message d'erreur est clair et indique comment corriger
- Le bouton d'envoi est désactivé tant que le formulaire contient des erreurs
- 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
- [] Créer
-
[] Task 2 : Implémenter la validation au blur (AC: 1)
- [] Écouter l'événement
blursur chaque champ - [] Valider le champ concerné
- [] Afficher/masquer l'erreur
- [] Écouter l'événement
-
[] 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
- [] Écouter l'événement
-
[] 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
/**
* 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) |
| 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 | - |
| Oui | - | 255 | ||
| 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) |