Files
Portfolio-Codex/docs/stories/5.2.validation-javascript.md
2026-02-04 21:06:19 +01:00

11 KiB
Raw Blame History

Story 5.2: Validation JavaScript Côté Client

Status

review

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

GPT-5 Codex

Implementation Plan

  • Implémenter les tâches 1 à 5 dans lordre avec tests à chaque étape.
  • Mettre à jour le formulaire pour les hooks JS (data-error, id submit).

File List

File Action Description
assets/js/contact-form.js Created Classe FormValidator avec validation complète
pages/contact.php Modified Hooks JS (data-error, submit-btn, script)
tests/contact-validation.test.php Added Tests du validateur JS (présence/méthodes)
tests/contact.test.php Modified Vérifications markup contact + data-error
tests/run.ps1 Modified Ajout du test contact-validation

Completion Notes

  • Task 1 : classe FormValidator et règles de validation mises en place (JS vanilla)
  • Task 2 : validation au blur + gestion des erreurs champ par champ
  • Task 3 : validation à la soumission et blocage si erreurs
  • Task 4 : messages d'erreur + bordures invalides configurés
  • Task 5 : désactivation/réactivation du bouton d'envoi
  • Tests : powershell -ExecutionPolicy Bypass -File tests/run.ps1

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)
2026-02-04 1.1 Validation JS côté client Amelia (Dev)