Créer une autocomplétion moderne avec fetch(), async/await et Symfony
L'autocomplétion fonctionne en 3 étapes simples :
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.
// 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(); } }
// 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);
Résultat : Une route /autocomplete/search?q=terme qui retourne du JSON propre !
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(); } }); } }
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);
}
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.
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();
}
}
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
}