31 - Symfony : Watchlist avec AJAX !
⚠️ Avant de commencer cette quête, tu dois avoir terminé les quêtes suivantes :
De plus, la quête ci-dessous peut être utile :
- Quête 845 (hors archive)
Introduction

Dans cette quête, tu vas rendre possible l'ajout de séries dans une "watchlist" à l’aide d’une requête asynchrone Javascript, également appelée :def[AJAX]{value="Asynchronous JavaScript And Xml"}.
🤓 À la fin de cette quête, tu seras capable de :
- ✅ Utiliser la fonction javascript
fetch - ✅ Manipuler des Promise
- ✅ Renvoyer une Response au format JSON
Et maintenant, en JS ;)
Pour le moment, tu as vu que les routes des contrôleurs pouvaient soit :
-
Renvoyer un template :
return $this->render('program/show.html.twig'); -
Rediriger vers une route :
return $this->redirectToRoute('program_show');
Une route de contrôleur peut également renvoyer des données au format JSON. Il sera alors possible de les manipuler dans le javascript.
Commence par modifier la réponse de ta méthode addToWatchlist() du contrôleur, pour renvoyer une réponse au format JSON. Pour cela, Symfony te propose une méthode disponible dans le contrôleur $this->json(). En argument de cette méthode, tu passeras un booléen indiquant si la série a été ajoutée ou retirée d'une watchlist.
return $this->json([
'isInWatchlist' => $this->getUser()->isInWatchlist($program)
]);
Dans le fonctionnement actuel, la page se recharge à chaque clic sur l'icône d'ajout. Tu aimerais que l'action d'ajouter à ta watchlist ne fasse pas se recharger la page.
Pour ça, tu vas utiliser la fonction Javascript fetch. Elle permet d'appeler des urls de façon asynchrone. Dans ton cas, tu vas pouvoir appeler la route program_watchlist sans avoir à recharger la page.
Rappel : tu as déjà découvert cette fonction en réalisant la quête Ajax - Requêtes asynchrones avec Fetch et Axios sur Odyssey.
Mais si l'utilisateur ne recharge plus la page, comment faire pour changer l'icône ? Encore une fois, c'est le javascript qui va s'en occuper.
Tu vas intercepter le clic sur la balise <a>, et personnaliser le comportement du lien pour rester sur la même page. Pour cela, tu vas avoir besoin d'ajouter à ton lien un id :
<a id="watchlist" data-no-swup href="{{ path('program_watchlist', {id: program.id})}}">
Note aussi l'ajout important de l'attribut data-no-swup. En effet, dans la quête précédente tu as configuré Swup avec Symfony UX. Swup "attrape" les clics sur des liens et génère une requête Ajax pour donner ce joli effet de transition. Mais là tu va vouloir générer ta propre requête Ajax sur l’icône de favori, donc cet attribut permet de désactiver Swup sur ce lien uniquement. Sans cela, l'affichage ne fonctionnera pas correctement.
Maintenant, tu peux passer au js ! Dans ton fichier /assets/app.js, ajoute un addEventListenner() afin d’écouter les clicks sur l'id watchlist. Lorsque l’événement click sera détecté, il faudra appeler la fonction addToWatchlist(event) que tu peux créer avec un console.log pour l'instant :
document.getElementById('watchlist').addEventListener('click', addToWatchlist);
function addToWatchlist(event) {
event.preventDefault();
console.log('Hello Watchlist !!!');
}
Tu noteras l'utilisation de event.preventDefault() qui va permettre de désactiver le fonctionnement normal du lien : en cliquant dessus, tu vas maintenant rester sur ta page.
Il faudra ensuite :
- Appeler la fonction
fetch('/url_a_appeler') - Changer la classe css
bi-heartparbi-heart-fillpour indiquer que la série a été ajoutée à la watchlist. Ceci changera l’icône de Bootstrap icons. Une fois un cœur creux, une fois un cœur rempli.
Fetch en action
La fonction javascript fetch se base sur le protocole HTTP. Elle permet d’effectuer des requêtes HTTP sur des urls avec la méthode GET par défaut. Dans notre cas, elle va nous permettre d’appeler une route de notre contrôleur.
Rappel : les routes de nos contrôleurs PHP répondent à des requêtes HTTP avec une URL et une METHOD.
La fonction Javascript fetch prend en paramètre une URL et renvoie une Promise ou ”promesse” qui correspond à un comportement attendu en cas de succès ou d’erreur de la requête. C’est une classe JavaScript qui représente la réussite ou l’échec d’une opération asynchrone. La fonction fait une requête à l'URL. La réponse est gérée par la Promise. Elle possède 2 méthodes :
then(): cette méthode sera appelée quand la requête sera terminée avec succès, codeHTTP_OK 200. Elle prend en paramètre une fonction qui recevra la réponse.catch(): cette méthode sera appelée si la requête a un code d'erreur commeHTTP_NOT_FOUND 404par exemple.
C'est parti ! Tu dois remplir l'URL que prend fetch en paramètre à l'aide de l'attribut de données href :
document.getElementById("watchlist").addEventListener("click", addToWatchlist);
function addToWatchlist(e) {
e.preventDefault();
const watchlistLink = e.currentTarget;
const link = watchlistLink.href;
// Send an HTTP request with fetch to the URI defined in the href
try {
fetch(link)
// Extract the JSON from the response
.then(res => res.json())
// Then update the icon
.then(data => {
const watchlistIcon = watchlistLink.firstElementChild;
if (data.isInWatchlist) {
watchlistIcon.classList.remove("bi-heart"); // Remove the .bi-heart (empty heart) from classes in <i> element
watchlistIcon.classList.add("bi-heart-fill"); // Add the .bi-heart-fill (full heart) from classes in <i> element
} else {
watchlistIcon.classList.remove("bi-heart-fill"); // Remove the .bi-heart-fill (full heart) from classes in <i> element
watchlistIcon.classList.add("bi-heart"); // Add the .bi-heart (empty heart) from classes in <i> element
}
});
} catch (err) {
console.error(err);
}
}
Maintenant, quand tu cliques sur l'icône, la page ne se recharge plus et la série est bien ajoutée à ta watchlist.
💪 Challenge
Ajout à la "Watchlist"
Le code réalisé lors de cette quête est fonctionnel. Tu peux ainsi permettre à un utilisateur, depuis la page descriptive d'une série, de l'ajouter / retirer de sa watchlist. C'était déjà un challenge en soi. Réalise une vidéo à partir de ton projet pour partager ta réussite ;)
Critères de validation
- L'ajout à la watchlist sur la page de détails fonctionne,
- La page ne se recharge pas lors du clic sur le cœur,
- Si la page est rafraîchie manuellement, le cœur garde son état plein/vide,
- Le code est disponible sur un repository GitHub, avec une branche correspondant à cette quête.