Préambule
 
  • Les démonstrations de ce TD se basent sur les sources disponibles [ ici ].
  • Attention, seul le répertoire src est dans l'archive. Il faut donc créer un projet avec vue-cli et remplacer son répertoire src.

1°/ Que sont les slots ?

  • Le principe des slots est relié à celui des props : il sert à passer des choses d'un composant parent à un fils.
  • Les props permettent de passer des valeurs,
  • Les slots permettent de passer du HTML+directives vuejs.
  • Le slots sont donc un moyen pour un composant parent de modifier l'apparence visuel d'un composant fils.
  • Encore faut-il que le fils soit implémenté pour autoriser cela, ce qui se fait grâce à des directives spéciale de vuejs.

 

2°/ Principes de création

Dans le composant fils :

  • Pour créer des slots dans un composant, on utilise la balise <slot> dans la partie template du composant.
  • Si on met quelque chose entre l'ouverture et la fermeture de <slot>, cela constitue le contenu par défaut au cas où le composant parent de donne pas de contenu.
  • Ce contenu par défaut peut bien entendu être défini grâce aux variables et aux props du composant fils.
  • Cette balise a un attribut name qui permet de donner un nom aux slots et ainsi différencier leur contenu.
  • On peut également créer une (et une seule) balise slot sans attribut name. C'est la balise par défaut. Son nom existe quand même : c'est default.
  • Remarque 1 : si un slot avec le même nom apparaît plusieurs fois dans le template du composant, le contenu donné par le parent sera copié/collé autant de fois (cf. exemple basique ci-dessous).
  • Remarque 2 : dans le cas de la remarque 1, on peut donner un contenu par défaut différent à chaque exemplaire du slot. En revanche, si le composant parent donne du contenu, ce sera le même pour tous les exemplaires.

Dans le composant parent :

  • Dans le composant parent, pour affecter quelque chose à un slot particulier, on utilise : <template v-slot:nom_slot> contenu </template>
  • Il existe également une syntaxe raccourcie pour v-slot:nom_slot : il suffit d'utiliser #nom_slot
  • S'il existe un slot par défaut, on peut simplement mettre le contenu, sans l'entourer par avec <template>.

 

Démonstration :
  • lancer npm run dev puis visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Montrer le code de TD7Demo1Compo.vue. On observe que le composant définit 4 slots, mais il y en a 2 qui ont le même nom, à savoir banner.
  • On remarque également que ces 2 slots ont un contenu par défaut différent, l'un étant créé à partir d'une variable de data.
  • Montrer le code de TD7Demo1View.vue. On observe que le contenu du slot par défaut de TD7Demo1Compo est définit sans utiliser <template v-slot:default>.
  • Le contenu du slot banner n'est pas défini (en commentaire), et celui du slot title contient un titre dont la valeur vient de la variable title.
  • Vu que TD7Demo1View ne définit pas le contenu de banner, ce sont les contenus par défaut qui sont affichés.
  • Décommenter la définition de banner => on voit apparaître 2 fois le contenu défini dans TD7Demo1View.

 

Question : vu qu'il existe une variable title aussi bien dans le père que le fils, laquelle va être utilisée pour remplir le slot ?

  • D'après ce qui s'affiche, on voit que c'est celle de TD7Demo1View qui est utilisée.
  • Normal car ce qui est virtuellement copié/collé dans le slot du composant fils n'est pas ce que le programmeur a écrit en HTML+vuejs dans TD7Demo1View, mais le résultat de son évaluation.
  • En fait, {{ title }} est écrit dans TD7Demo1View, donc évalué dans TD7Demo1View, ce qui utilise la variable title de TD7Demo1View. C'est le résultat de cette évaluation est utilisé dans le slot de TD7Demo1Compo.

 

 Remarques :

  • On peut mettre les template de slot dans n'importe quel ordre.
  • On remarque que le contenu qui n'est pas mis entre <template> est bien copié/collé dans le slot par défaut.

 

3.2°/ Elargir la "portée" des slots : les scoped-slots

 

3.2.1°/ Principes et utilité

  • Dans la démonstration précédente, le contenu du slot title est défini dans App en utilisant la variable title de TD7Demo1View.
  • Est-il possible de définir ce contenu grâce à la variable title de TD7Demo1Compo ? A priori, non.
  • En effet, le composant père n'a pas accès directement aux données du composant fils.
  • Pourtant, cela serait bien pratique pour les slots.
  • En effet, prenons un composant A qui contient un composant B. Ce dernier affiche une liste à partir de ses données propres.
  • De base, la façon dont s'affiche cette liste est figée par le template du composant B.
  • Comment A peut-il influer sur cet affichage ?
  • Une solution possible est de prévoir des props dans B, que A va pouvoir fixer et qui vont influer sur l'affichage.
  • Mais ce n'est pas une solution universelle car même si on peut ainsi changer le style voire choisir un ensemble de balises à utiliser pour créer la liste, on ne peut pas prévoir toute les façon d'afficher cette liste (à puce, en tableau, en ligne, ...)
  • La solution universelle est apportée par une catégorie spéciale de slot, appelée les scoped-slots.

 

  • Un scoped-slot est un slot pour lequel on définit un certain nombre de variables et/ou fonctions du composant fils auxquelles le père va avoir accès pour définir le contenu du slot (et pas un autre).
  • Dans l'exemple du composant B, on voudrait par exemple fournir à A chaque item de la liste. Comme ça, A décide totalement du rendu de chaque item.
  • Par exemple, si chaque item est en fait un objet contenant plusieurs champs, le composant A pourrait décider lesquels s'affichent.
  • Il pourrait aussi décider de ne pas afficher certains items, ou encore d'afficher des les items avec des styles différents, ...

 

3.2.2°/ Création

  • Un scoped-slot ne rend accessible (= publie) à son composant parent que des variables locales ou des fonctions.
  • NB : il n'y a aucun intérêt publier une props (déjà accessible dans le composant parent) ou une valeur fixe.
  • Pour publier, on utilise dans la balise <slot> des instructions v-bind:nom_publication="nom_variable/fonction".
  • Cette syntaxe permet de mettre un nom de publication différent de celui de la variable/fonction. Mais généralement, on prend le même nom. Exemples des 2 cas :
    • <slot name="myslot1" :list="items"></slot> : le composant fils rend accessible sa variable items au composant père, sous le nom list.
    • <slot name="myslot2" :shops="shops"></slot> : le composant fils rend accessible sa variable shops au composant père, avec le même nom shops.
  • Le composant fils peut publier plusieurs variables/fonctions. Pour cela, on utilise autant de fois v-bind qu'il y a de variables à publier. Par exemple :
    • <slot name="myslot" :messages="messages" :selection="selected"></slot>
  • Toutes les variables publiées vont être regroupées dans un unique objet qui va être accessible au composant père.

 

 3.2.3°/ Définition dans le composant parent

  • Généralement, on ne récupère pas directement l'objet publié par le slot dans le composant parent, mais plutôt les champs de cet objet, donc les variables qui ont été publiées par le composant fils.
  • Pour faire cela, on utilise l'opérateur de décomposition d'objet de JS : { champ1, champ2, ...}

Exemple :

let obj = {txt:"hello", val: 4}
let {txt, val} = obj
console.log(txt+" "+val)
  • Dans cet exemple, on décompose les champs d'un objet obj et on les affecte à des variables de même nom, donc txt et val.

 

  • C'est ce principe que l'on utilise dans le composant père pour accéder à tout ou une partie de ce qui a été publié par un slot, avec la syntaxe <template #nom_slot="{nom1, nom2, ...}">
  • Le composant père peut ensuite utiliser directement nom1, nom2, ... pour définir le contenu du slot.

 

3.2.4°/ Exemple synthétique

  • dans le fils CompoFils.vue :
<template>
  ...
  <slot name="myslot" :msg="msg" :val="x" :isChecked="showSel">
  ...
</template>
  •  dans le père :
<template>
  ...
  <CompoFils>
    <template #myslot="{msg, val, isChecked}">
      <p>msg = {{msg}}</p>
      <p>val = {{val}}</p>
      <input type="checkbox" :checked="isChecked">
    </template>
  </CompoFils>
  ...
</template>

 

Démonstration :
  • lancer npm run dev puis cliquer sur le bouton Demo 2 et visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Montrer le code de TD7Demo2View.vue. On observe que le composant définit le slot nommé list-item de TD7Demo2Compo.
  • Comme le slot permet de définir le contenu de <li> et que cette balise est répétée, le slot va être également répété.
  • Cela permet pour chaque occurrence de publier la même variable item, mais avec une valeur différente.
  • On remarque également qu'il est parfaitement possible de publier une fonction.
  • Problème : dans l'exemple précédent, on a vu que plusieurs exemplaires du même slot sont remplis avec le même contenu.
  • Mais grâce au principe des scoped-slot, le composant père va recevoir une valeur différente pour item.
  • En récupérant cette variable item, le père peut customiser différemment chaque exemplaire du slot !

 

3.3°/ vue-router et slots

  • Quand vue-router est utilisé pour afficher un composant, il est possible que ce composant puisse contenir des slots. Comment spécifier le contenu de ces slots ?
  • Techniquement, le composant parent est la balise <router-view>. On devrait donc utiliser <template> à l'intérieur de <router-view>
  • Cette solution fonctionne avec vue-router pour vuejs V2 mais pas en V3 !
  • Fort heureusement, les concepteur de vue-router ont proposé une méthode pour récupérer le composant qui va apparaître (NB : grâce à un scoped-slot).
  • On peut ensuite créer une "indirection" grâce à la balise <component> et son attribut special is, pour que le composant ne soit plus de fils de <router-view>, mais celui de <component>
  • Ainsi, on peut définir les slots du composant à l'intérieur de <component>.
  • NB : on peut également passer des valeurs de props au composant en donnant leur valeur dans les attributs de <component>.

Exemple :

<template>
  ...
  <router-view v-slot="{Component}">
    <component :is="Component" props1="..." props2="...">
      <template #myslot> ... </template>
    </component>
  </router-view>
  ...
</template>

 

Démonstration :
  • lancer npm run dev puis cliquer sur le bouton Demo 2 et visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Cliquer sur le bouton pour suivre la route de niveau 2. Cela permet de faire apparaître un nouveau composant.
  • Un message par défaut apparaît en bas de ce composant, qui peut être customisé par un scoped-slot.
  • Cliquer sur la case à cocher : le contenu du slot est modifié.
  • Montrer le code de TD7Demo2View.vue. On observe que le composant utilise <router-view> 
  • Montrer les routes : la route de second niveau permet d'afficher TD7SubView
  • Ce dernier définit un scoped-slot custom-msg, qui publie le contenu de la variable msg à son père sous le nom content.
  • Dans TD7Demo2View.vue , on utilise le principe de l'exemple ci-dessus pour définir le contenu de ce slot si la case est cochée.
  • En l'occurrence, cela modifie le message publié par le fils et l'affiche en gras.

 

4°/ combinaison props et slots

  • Dans la démonstration précédente, on constate que le tableau des items est défini dans le composant fils.
  • D'un point de vue conception, ce n'est pas réaliste. Il est quasi certain que cette liste devrait être définie chez le père, notamment parce que d'autres composants fils pourraient en avoir besoin.
  • Pour régler le problème, il suffit que le père donne cette liste en props à ses fils.
  • Mais si on utilise en plus les scoped-slots, on arrive à une situation "bizarre" :
    • le père donne la liste en props au fils,
    • le fils utilise cette props pour créer un scoped-slot par item, afin de passer chaque item au père.
    • le père récupère chaque item publié et l'utilise pour définir le contenu du slot correspondant.
  • Cela semble inutile que le fils donne accès aux items au père puisque ce dernier y a déjà accès.
  • Cependant, cet accès ne permet pas une manipulation comme si c'était une variable locale. Cela ne sert que dans la partie <template> et c'est seulement pour définir le contenu du slot.
  • En conclusion, même si ce semble un peu compliqué, la combinaison props+scoped-slot est un moyen très puissant pour fournir des données ET l'apparence de celles-ci à un composant fils.
  • Par exemple, ce principe est très utilisé dans la bibliothèque de composants graphiques vuetify.
Démonstration :
  • lancer npm run dev, cliquer sur le bouton Dem 3, puis visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Un clic sur "buy" fait disparaître l'objet de TD7Demo3CompoA et il apparaît dans TD7Demo3CompoB.
  • C'est l'inverse quand on clique sur "sell".
  • Dans chaque cas, le montant d'or est actualisé.
  • Les items et leur prix de base sont définis dans TD7Demo3View. En revanche, le calcul des promotion est fait dans une méthode du composant fils TD7Demo3CompoA.
  • Dans ce dernier, la façon d'afficher un item est customisable via scoped-slot, qui publie vers le père l'item ainsi que la fonction de calcul des promotions.
  • TD7Demo3View peut donc utiliser ces deux informations pour modifier le visuel par défaut, et ainsi afficher le nom, le prix normal et remisé.
  • Quand on clique sur le bouton d'achat d'un item dans TD7Demo3CompoA, un événement est émis, qui est capturé par TD7Demo3View.
  • Cela appel la fonction d'achat qui enlève l'item acheté de la liste des items en stock et le met dans celle des achetés. Comme la première est donnée en props à TD7Demo3CompoA, l'item disparaît pour apparaître dans TD7Demo3CompoB. L'inverse se produit quand on revend l'item.
  • A noter que l'événement d'achat contient le prix remisé, ce qui permet de déduire le solde d'or disponible (cf. fonction buy() ). En cas de revente, on se base sur le prix normal pour ajouter au solde => on s'enrichit.