1 / 8

🚀 Autocomplétion Ajax avec Symfony

Pour développeurs web débutants

Créer une autocomplétion moderne avec fetch(), async/await et Symfony

🎯 Objectifs de cette présentation :

  • Comprendre comment créer une autocomplétion sans API séparée
  • Maîtriser fetch() avec async/await en JavaScript moderne
  • Intégrer facilement avec Symfony
  • Optimiser les performances avec le debounce

🏗️ Architecture de la solution

Principe général

L'autocomplétion fonctionne en 3 étapes simples :

💡 Avantage clé

Pas besoin de créer une API complète ! On utilise simplement une route Symfony classique qui retourne du JSON au lieu d'une vue Twig.

Technologies utilisées

🎮 Côté Symfony : Le Controller

Création de la route d'autocomplétion

// src/Controller/AutocompleteController.php
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class AutocompleteController extends AbstractController
{
    #[Route('/autocomplete/search', name: 'autocomplete_search', methods: ['GET'])]
    public function search(Request $request): JsonResponse
    {
        $query = $request->query->get('q', '');
        
        // Minimum 2 caractères pour chercher
        if (strlen($query) < 2) {
            return new JsonResponse([]);
        }
        
        // Requête en base de données
        $results = $this->entityManager
            ->getRepository(YourEntity::class)
            ->createQueryBuilder('e')
            ->where('e.name LIKE :query')
            ->setParameter('query', '%' . $query . '%')
            ->setMaxResults(10)
            ->getQuery()
            ->getResult();
    }
}

🎮 Controller : Formatage des données

    // Suite de la méthode search()
    
    // Formatage des résultats pour le frontend
    $data = [];
    foreach ($results as $result) {
        $data[] = [
            'id' => $result->getId(),
            'label' => $result->getName(), // Ce qui s'affiche
            'value' => $result->getName()  // Ce qui va dans l'input
        ];
    }
    
    return new JsonResponse($data);
            

Points importants

Résultat : Une route /autocomplete/search?q=terme qui retourne du JSON propre !

⚡ JavaScript : Structure de la classe

Classe Autocomplete moderne

class Autocomplete {
    constructor(inputElement, resultsElement) {
        this.input = inputElement;
        this.results = resultsElement;
        this.debounceTimer = null; // Pour optimiser
        
        this.init();
    }
    
    init() {
        // Écouter les changements dans l'input
        this.input.addEventListener('input', (e) => {
            this.handleInput(e.target.value);
        });
        
        // Masquer quand on clique ailleurs
        document.addEventListener('click', (e) => {
            if (!this.input.contains(e.target) && 
                !this.results.contains(e.target)) {
                this.hideResults();
            }
        });
    }
}

Avantages de cette approche

⚡ JavaScript : Optimisation avec Debounce

    handleInput(query) {
        // Annuler la requête précédente
        clearTimeout(this.debounceTimer);
        
        // Minimum 2 caractères
        if (query.length < 2) {
            this.hideResults();
            return;
        }
        
        // Attendre 300ms avant de chercher
        this.debounceTimer = setTimeout(() => {
            this.search(query);
        }, 300);
    }

🚀 Pourquoi le debounce ?

Sans debounce, chaque lettre tapée déclenche une requête. Avec "symfony" ça fait 7 requêtes !

Avec debounce de 300ms : 1 seule requête quand l'utilisateur arrête de taper.

Autres optimisations possibles

⚡ JavaScript : Requête avec fetch async/await

    async search(query) {
        try {
            // Requête moderne avec fetch
            const response = await fetch(
                `/autocomplete/search?q=${encodeURIComponent(query)}`, 
                {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Requested-With': 'XMLHttpRequest'
                    }
                }
            );
            
            // Vérifier si la réponse est OK
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            // Convertir en JSON
            const data = await response.json();
            this.displayResults(data);
            
        } catch (error) {
            console.error('Erreur lors de la recherche:', error);
            this.hideResults();
        }
    }

Points clés de fetch()

⚡ JavaScript : Affichage des résultats

    displayResults(results) {
        if (results.length === 0) {
            this.hideResults();
            return;
        }
        
        let html = '';
        results.forEach(item => {
            html += `
                <div class="autocomplete-item" 
                     data-id="${item.id}" 
                     data-value="${item.value}">
                    ${item.label}
                </div>
            `;
        });
        
        this.results.innerHTML = html;
        this.results.style.display = 'block';
        
        // Gérer les clics
        this.results.querySelectorAll('.autocomplete-item').forEach(item => {
            item.addEventListener('click', () => {
                this.selectItem(item);
            });
        });
    }
    
    selectItem(item) {
        this.input.value = item.dataset.value;
        this.hideResults();
        // Ici vous pouvez ajouter votre logique personnalisée
    }