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

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

  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

/**
 * 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)