Commentaires
Je te préviens, cette partie est super intéressante et super pratique !

Arthur, l'apprenti développeurChouette !

Le but et les bases


Les slots permettent de passer du contenu d'un parent à un enfant.

Arthur, l'apprenti développeurMais ce n'est le rôle des props ça ?
Aussi si ! Mais voici un exemple pour que tu comprennes mieux. J'ai ce contenu HTML :

{"language":"text/html","content":"<h2>Ma recette de cuisine</h2>\n<h3>Ingrédients</h3>\n<ul>\n\t<li>...</li>\n</ul>\n<h3>Description</h3>\n<p>...</p>","filename":""}

J'aimerais le mettre dans une card. J'ai déjà conçu cet élément dans un composant que j'ai appelé "card". Passer ce contenu HTML sous forme de props ce n'est quand même pas pratique... Ce serait bien de pouvoir faire la chose suivante :

{"language":"text/html","content":"<card>\n\t<h2>Ma recette de cuisine</h2>\n\t<h3>Ingrédients</h3>\n\t<ul>\n\t\t<li>...</li>\n\t</ul>\n\t<h3>Description</h3>\n\t<p>...</p>\n</card>","filename":""}

Hé bien, tu l'auras deviné : les slots répondent à ce besoin !

On peut passer du contenu HTML à un composant de tel sorte que celui-ci le réutilise via les slots.
Prenons le composant card suivant par exemple :

{"language":"text/html","content":"<template>\n\t<div class=\"card\">\n\t</div>\n</template>\n\n<style scoped>\n\t.card{\n\t\tborder:1px solid black;\n\t\tpadding: 10px 15px;\n\t}\n</style>","filename":"Card.vue"}

Pour que ce composant réutilise le contenu qu'on lui passe, on va utiliser la balise <slot> comme suit :

{"language":"text/html","content":"<template>\n\t<div class=\"card\">\n\t\t<slot></slot>\n\t</div>\n</template>\n\n<style scoped>\n\t.card{\n\t\tborder:1px solid black;\n\t\tpadding: 10px 15px;\n\t}\n</style>","filename":"Card.vue"}

Et prenons le code suivant :

{"language":"text/html","content":"<card>\n\t<h2>Ma recette de cuisine</h2>\n\t<h3>Ingrédients</h3>\n\t<ul>\n\t\t<li>...</li>\n\t</ul>\n\t<h3>Description</h3>\n\t<p>...</p>\n</card>","filename":"App.vue"}

Voici le résultat :


On voit bien que notre code HTML correspondant à la recette a été intégré au sein du composant, à la place de <slot> !

Arthur, l'apprenti développeurJ'ai compris !

Les slots nommés


Nous allons maintenant répondre à une nouvelle problématique. Imaginons que notre composant card soit maintenant le suivant :

{"language":"text/html","content":"<template>\n\t<div class=\"card\">\n\t\t<h2 class=\"card__title\"></h2>\n\t\t<div class=\"card__content\"></div>\n\t</div>\n</template>\n\n<style scoped>\n\t.card{\n\t\tborder:1px solid black;\n\t\tpadding: 10px 15px;\n\t}\n</style>","filename":"Card.vue"}

Nous voyons que nous avons maintenant un emplacement pour un titre et un emplacement pour le contenu de la card.

Arthur, l'apprenti développeurJe vois où tu veux en venir... On ne peut pas utiliser <slot> </slot> car il faudrait plutôt un slot pour le titre, un slot pour le contenu c'est cela ?
Tout à fait. Si on utilise encore <slot> </slot> et qu'on le met dans la div card__content par exemple, le titre va toujours rester vide, et inversement.
Pour palier à ce problème, on peut utiliser les slots nommés. Cela nous permet d'utiliser plusieurs slots au sein d'un même composant. Pour les utiliser il nous suffit de rajouter l'attribut name="" sur les balises slots et de choisir des noms différents.
Notre composant card modifié ressemblera donc à :

{"language":"text/html","content":"<template>\n\t<div class=\"card\">\n\t\t<h2 class=\"card__title\">\n\t\t\t<slot name=\"title\"></slot>\n\t\t</h2>\n\t\t<div class=\"card__content\">\n\t\t\t<slot name=\"content\"></slot>\n\t\t</div>\n\t</div>\n</template>\n\n<style scoped>\n\t.card{\n\t\tborder:1px solid black;\n\t\tpadding: 10px 15px;\n\t}\n</style>","filename":"Card.vue"}

Arthur, l'apprenti développeurSuper ! Mais comment fait-on pour passer le contenu de notre composant parent au sein de ces slots nommés ?
On va utiliser une nouvelle directive : v-slot. Elle a une syntaxe un peu différente des directives vues précédemment dans le cours. Je te laisse regarder par toi-même :

{"language":"text/html","content":"<card>\n <template v-slot:title>Ma recette de cuisine</template>\n <template v-slot:content>\n <h3>Ingrédients</h3>\n <ul>\n <li>...</li>\n </ul>\n <h3>Description</h3>\n <p>...</p>\n </template>\n</card>","filename":"App.vue"}

Arthur, l'apprenti développeurEn effet, drôle de syntaxe avec ces deux-points ":" ! Pourquoi utilise-t-on la balise template ?
La balise template permet juste d'englober, elle ne rajoutera aucune balise (div, p...) dans le rendu. C'est très pratique ;).

Une autre information qui a son importance : le slot par défaut qui s'utilise avec <slot> </slot> a aussi un nom : default. Ainsi, on peut dans un composant parent faire <template v-slot:default></template> ;)

Arthur, l'apprenti développeurC'est noté !
Bon, je dois t'avouer qu'on a encore une problématique à résoudre. Notamment si on veut utiliser les slots au sein de notre projet pour pouvoir utiliser notre slider. Pour bien comprendre le souci, on va devoir réfléchir à la portée des slots. Leur scope.

Agir sur le scope


Ce qu'il faut savoir, c'est que chaque composant a accès par défaut à son propre contenu dans le cadre des slots.

Arthur, l'apprenti développeurOui ça parait logique, je ne vois pas le problème ?
Tu vas vite le voir...
Dans le cadre de notre projet on veut que les photos s'affichent dans notre slider. Le but des composants est que ceux-si soient réutilisables. Ainsi, hors de question de mettre directement dans le composant Slider une quelconque architecture HTML propre à nos photos. Non, il est important que chaque slide du Slider soit indépendante du composant et qu'on puisse ainsi les designer comme on le souhaite depuis le composant parent. Cela implique d'utiliser un slot au sein de notre composant Slider. J'en profite aussi pour passer les slides sous forme de props. Voici donc le résultat :

{"language":"text/html","content":"<template>\n <div>\n <button @click=\"slide(-1)\">Aller à gauche</button>\n <button @click=\"slide(1)\">Aller à droite</button>\n <template v-for=\"(slide, key) in slides\" :key=\"key\">\n <div v-if=\"index === key\">\n <slot></slot>\n </div>\n\n </template>\n </div>\n</template>\n\n<script>\n export default {\n props:['slides'],\n data() {\n return {\n index:0\n }\n },\n methods:{\n slide(operation) {\n // Car % n'est pas le modulo mathématique en JS\n // on doit faire un code un peu compliqué\n // pour gérer les nombres négatifs\n this.index = (((this.index + operation)%this.slides.length)+this.slides.length)%this.slides.length\n }\n },\n }\n</script>","filename":"Slider.vue"}

Du coup, on va modifier notre App.vue en conséquence :

{"language":"text/html","content":"<template>\n\t<!-- Le composant photo actuellement -->\n <photo \n v-for=\"photoLoop in photos\" \n :key=\"photoLoop\" \n v-bind=\"photoLoop\"\n ></photo>\n\t\n\t<!-- Le slider -->\n <slider :slides=\"photos\">\n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-17&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider\n },\n\n data() {\n return {\n photos: [],\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n }\n}\n</script>\n","filename":"App.vue"}

Arthur, l'apprenti développeurOk on passe les photos en props. Mais là notre slider ne sert à rien, on affiche encore les photos comme avant avec v-for sur le composant Photo ?
Tout à fait. Je l'ai laissé pour que tu comprennes bien. Ce qu'on aimerait faire c'est passer sous forme de slot le composant Photo à notre slider.

Arthur, l'apprenti développeurJe comprends ! Voici ce que ça donne :

{"language":"text/html","content":"<template>\n\t<!-- Le slider -->\n <slider :slides=\"photos\">\n\t\t<photo \n v-for=\"photoLoop in photos\" \n :key=\"photoLoop\" \n v-bind=\"photoLoop\"\n \t></photo>\n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-17&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider\n },\n\n data() {\n return {\n photos: [],\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n }\n}\n</script>\n","filename":"App.vue"}


As-tu testé ce code ?

Arthur, l'apprenti développeurJe viens de le faire... Ce n'est pas le résultat attendu en effet. Là on a toutes les photos qui s'affichent sur une slide du Slider... On n'a pas une photo par slide...
Tout à fait. Je rappelle que le v-for pour chaque photo est maintenant au sein du composant Slider vu qu'on a une photo par slide !

Arthur, l'apprenti développeurJe comprends ! Il faut donc préciser au sein de App.vue non pas une boucle sur les photos mais seulement le HTML pour une seule photo ! Voici ce que ça donne :

{"language":"text/html","content":"<template>\n\t<!-- Le slider -->\n <slider :slides=\"photos\">\n\t\t<photo></photo>\n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-17&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider\n },\n\n data() {\n return {\n photos: [],\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n }\n}\n</script>\n","filename":"App.vue"}

Ah mais attends... Comment je passe au composant Photo les props maintenant ?

Bingo ! Tu en es arrivé à la conclusion que je voulais : on a un souci avec l'utilisation de slot. Ce souci vient du fait qu'on a besoin de données du composant enfant (Slider) dans le composant parent (App). On a en effet besoin de la photo qui vient de la boucle suivante dans notre composant App :

{"language":"text/html","content":"<template v-for=\"(slide, key) in slides\" :key=\"key\">\n\t<div v-if=\"index === key\">\n\t\t<!-- Ici j'ai accès à une photo. C'est le paramètre \"slide\" -->\n\t\t<slot></slot>\n\t</div>\n</template>","filename":"Slider.vue"}

Et c'est ce que je te disais :
Ce qu'il faut savoir, c'est que chaque composant a accès par défaut à son propre contenu dans le cadre des slots.

Arthur, l'apprenti développeurJe vois maintenant où tu voulais en venir... App n'a pas accès aux données de Slider, sauf qu'on en a besoin...
Exactement. Heureusement, notre framework préféré a pensé à ce cas d'utilisation. Un enfant peut laisser accès à plusieurs de ses données à son parent grâce à ce qu'on appelle les scoped slots. Tout repose sur notre directive préférée qui a plein de ressources. Une idée ?

Arthur, l'apprenti développeurv-bind ?
Tout à fait. Dans le composant enfant (ici Slider) on va v-bind les données qu'on veut passer au parent directement sur le slot concerné. Ainsi, on a :

{"language":"text/html","content":"<template>\n <div>\n <button @click=\"slide(-1)\">Aller à gauche</button>\n <button @click=\"slide(1)\">Aller à droite</button>\n <template v-for=\"(slide, key) in slides\" :key=\"key\">\n <div v-if=\"index === key\">\n <slot :slide=\"slide\"></slot>\n\t\t\t\t<!-- On peut v-bind autant de données qu'on veut.\n\t\t\t\t\tOn aurait pu donc aussi passer d'autres données.\n \t\t\t\t\tPar exemple : \n\t\t\t\t-->\n\t\t\t\t<!--\n\t\t\t\t<slot :slide=\"slide\" :key=\"key\" :indexCourant=\"index\"></slot>\n\t\t\t\t-->\n </div>\n\n </template>\n </div>\n</template>\n\n<script>\n export default {\n props:['slides'],\n data() {\n return {\n index:0\n }\n },\n methods:{\n slide(operation) {\n // Car % n'est pas le modulo mathématique en JS\n // on doit faire un code un peu compliqué\n // pour gérer les nombres négatifs\n this.index = (((this.index + operation)%this.slides.length)+this.slides.length)%this.slides.length\n }\n },\n }\n</script>","filename":"Slider.vue"}

Ces données sont maintenant récupérables au sein du composant parent grâce au template correspondant au slot. Dans le cadre de notre slider vu qu'on n'a qu'un seul slot, le template correspondant est default. On va alors modifier un peu la syntaxe qu'on a vue :

{"language":"text/html","content":"<template v-slot:default>\n</template","filename":""}

En rajoutant une valeur à notre v-bind qui sera un objet contenant les attributs passés par l'enfant au sein de ce slot. En général on l'appelle "slotProps" :

{"language":"text/html","content":"<template v-slot:default=\"slotProps\">\n</template","filename":""}

Et maintenant, on peut accéder aux données en faisant slotsProps.donnee. Dans notre cas :

{"language":"text/html","content":"<template>\n <slider :slides=\"photos\">\n <template v-slot:default=\"slotProps\">\n <photo \n v-bind=\"slotProps.slide\"\n ></photo>\n\t\t\t<!-- Si on avait passé les 2 autres données key et index on aurait pu y accéder : \n\t\t\t-->\n\t\t\t<!--\n\t\t\t{{ slotProps.key }}\n\t\t\t{{ slotProps.indexCourant }}\n\t\t\t-->\n\t\t\t\n\t\t</template>\n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-17&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider\n },\n\n data() {\n return {\n photos: [],\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n }\n}\n</script>\n","filename":"App.vue"}

Arthur, l'apprenti développeurJe t'avouerais que ce n'était pas simple, il va falloir que je relise !
Oui, je sais que c'est un peu compliqué. Cependant, c'est vraiment quelque chose à prendre en main. Je t'encourage à lire, lire, relire... Et à poser tes questions si tu en as évidemment. Car on va encore rajouter quelques informations.

Arthur, l'apprenti développeurEncore ?!
Oui mais pour ton bien, car on va améliorer la lisibilité du code :p. Actuellement le code est peu compréhensible : on passe une slotProps.slide à notre composant Photo, on comprend pas trop ce qu'il se passe si on met le nez sur le code la première fois. "Qu'est-ce que slotProps.slide ?" va se demander une personne qui voit le code la première fois (ou toi si tu n'as pas revu ton code depuis des mois èé !).
Nous pouvons donc rendre le code plus lisible en utilisant le "Destructuring assignment" offert par ES6. On peut donc transformer le nom des données passées par notre enfant au moment où on le récupère dans le parent. Ce serait sympa de modifier la donnée qui a pour nom "slide" (et qui contient les données d'une photo dans notre cas) par le nom "photo". Voici comment faire :

{"language":"text/html","content":"<template>\n <slider :slides=\"photos\">\n <template v-slot:default=\"{slide : photo}\">\n <photo \n v-bind=\"photo\"\n ></photo>\n </template>\n \n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-17&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider\n },\n\n data() {\n return {\n photos: [],\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n }\n}\n</script>\n","filename":"App.vue"}

Arthur, l'apprenti développeurC'est vrai que c'est plus clair...

Syntaxe raccourcie


Allez un dernier point rapidement. Peut être que tu t'en doutais, Vue.JS nous offre une syntaxe raccourcie pour la directive v-slot au même titre que v-bind devient " : " ou que v-on devient "@". On utilisera ici le dièse "#". v-slot:default devient #default ;).

Voici donc notre code terminé avec une légère modification en utilisant "key" pour que, peut être, cela te permette de mieux comprendre quand tu reliras le code :

{"language":"text/html","content":"<template>\n <slider :slides=\"photos\">\n <template #default=\"{slide : photo, key : index}\">\n\t\t\t<h2>Photo {{ index+1 }}/{{ photos.length }}</h2>\n <photo \n v-bind=\"photo\"\n ></photo>\n </template>\n \n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-17&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider\n },\n\n data() {\n return {\n photos: [],\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n }\n}\n</script>\n","filename":"App.vue"}

{"language":"text/html","content":"<template>\n <div>\n <button @click=\"slide(-1)\">Aller à gauche</button>\n <button @click=\"slide(1)\">Aller à droite</button>\n <template v-for=\"(slide, key) in slides\" :key=\"key\">\n <div v-if=\"index === key\">\n <slot :slide=\"slide\" :key=\"key\"></slot>\n </div>\n\n </template>\n </div>\n</template>\n\n<script>\n export default {\n props:['slides'],\n data() {\n return {\n index:0\n }\n },\n methods:{\n slide(operation) {\n this.index = (((this.index + operation)%this.slides.length)+this.slides.length)%this.slides.length\n }\n },\n }\n</script>","filename":"Slider.vue"}

Arthur, l'apprenti développeurJe vais relire ça plusieurs fois ! J'en ai besoin !
Oui il faut bien comprendre cette partie. C'est un cas d'utilisation très courant ! Les slots sont au coeur de la logique des composants. N'hésite pas à prendre un peu de temps sur ce chapitre si tu en ressens le besoin. Tu peux aussi te reposer un peu en embellissant notre application qui pour le moment est un peu... "moche", avouons-le !

Arthur, l'apprenti développeurEntendu, ça permettra de reposer mon cerveau en ébullition de faire un peu de CSS.
Ça marche ! Quand tu te sens prêt on se revoit pour voir la communication enfant -> parent cette fois-ci pour rajouter une modale sur notre application ! J'ai terminé cette partie
Demander de l'assistance