Passer au contenu

Slots

Cette page suppose que vous avez déjà lu les bases à propos des composants. Lisez-les d'abord si vous débutez avec les composants.

Les contenus de slot

Nous avons appris que les composants peuvent accepter des props, qui peuvent être des valeurs JavaScript de n'importe quel type. Mais qu'en est-il du contenu du template ? Dans certains cas, nous pouvons vouloir transmettre un fragment de template à un composant enfant et laisser le composant enfant afficher le fragment dans son propre template.

Par exemple, nous pouvons avoir un composant <FancyButton> qui prend en charge l'utilisation suivante :

template
<FancyButton>
  Click me! <!-- contenu du slot -->
</FancyButton>

Le template de <FancyButton> ressemble à ceci :

template
<button class="fancy-btn">
  <slot></slot> <!-- emplacement du slot -->
</button>

L'élément <slot> est un emplacement du slot qui indique où le contenu du slot fourni par le parent doit être affiché.

slot diagram

Et le DOM rendu final :

html
<button class="fancy-btn">Click me!</button>

Avec les slots, le <FancyButton> est responsable du rendu du <button> externe (et de son style), tandis que le contenu interne est fourni par le composant parent.

Une autre façon de comprendre les slots consiste à les comparer aux fonctions JavaScript :

js
// composant parent passant le contenu du slot
FancyButton('Click me!')

// FancyButton effectue le rendu du contenu du slot dans son propre template
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

Le contenu du slot ne se limite pas à du texte. Il peut s'agir de n'importe quel contenu de template valide. Par exemple, nous pouvons passer plusieurs éléments, voire d'autres composants :

template
<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

En utilisant des slots, notre <FancyButton> est plus flexible et réutilisable. Nous pouvons maintenant l'utiliser à différents endroits avec un contenu différent, mais tous avec le même style appliqué.

Le mécanisme de slot des composants Vue est inspiré de l'élément natif Web Component <slot>, mais avec des fonctionnalités supplémentaires que nous verrons plus tard.

Portée du rendu

Le contenu du slot a accès à la portée des données du composant parent, car il est défini dans le parent. Par exemple :

template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

Ici, les deux interpolations {{ message }} rendront le même contenu.

Le contenu du slot n'a pas accès aux données du composant enfant. Les expressions dans les templates Vue ne peuvent accéder qu'à la portée de déclaration dans laquelle elles sont définies, conformément à la portée lexicale de JavaScript. Autrement dit :

Les expressions présentes dans le template du parent n'ont accès qu'à la portée du parent ; les expressions dans le template de l'enfant n'ont accès qu'à la portée du composant enfant.

Contenu par défaut

Il existe des cas où il est utile de spécifier un contenu par défaut pour un slot, à rendre uniquement lorsqu'aucun contenu n'est fourni. Par exemple, dans un composant <SubmitButton> :

template
<button type="submit">
  <slot></slot>
</button>

Nous pourrions souhaiter que le texte "Submit" soit rendu à l'intérieur du <button> si le parent n'a fourni aucun contenu pour le slot. Pour faire de "Submit" le contenu par défaut, nous pouvons le placer entre les balises <slot> :

template
<button type="submit">
  <slot>
    Submit <!-- contenu par défaut -->
  </slot>
</button>

Maintenant, lorsque nous utilisons <SubmitButton> dans un composant parent, en ne fournissant aucun contenu pour le slot :

template
<SubmitButton />

Cela rendra le contenu par défaut, "Submit":

html
<button type="submit">Submit</button>

Mais si nous fournissons le contenu :

template
<SubmitButton>Save</SubmitButton>

Alors, le contenu fourni sera affiché à la place :

html
<button type="submit">Save</button>

Slots nommés

Il y a des moments où il est utile d'avoir plusieurs emplacements de slot dans un seul composant. Par exemple, dans un composant <BaseLayout> avec le template suivant :

template
<div class="container">
  <header>
    <!-- Nous voulons le contenu du header ici -->
  </header>
  <main>
    <!-- Nous voulons le contenu principal ici -->
  </main>
  <footer>
    <!-- Nous voulons le contenu du footer ici -->
  </footer>
</div>

Dans ces cas, l'élément <slot> a un attribut spécial, name, qui peut être utilisé pour attribuer un ID unique à différents slots afin que vous puissiez déterminer où le contenu doit être affiché :

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Une balise <slot> sans attribut name porte implicitement le nom "default".

Dans un composant parent utilisant <BaseLayout>, nous avons besoin d'un moyen de transmettre plusieurs fragments de contenu de slot, chacun ciblant un emplacement de slot différent. C'est là qu'interviennent les slots nommés.

Pour passer un slot nommé, nous devons utiliser un élément <template> avec la directive v-slot, puis passer le nom du slot comme argument à v-slot :

template
<BaseLayout>
  <template v-slot:header>
    <!-- contenu pour le slot header -->
  </template>
</BaseLayout>

v-slot a un raccourci dédié #, donc <template v-slot:header> peut être raccourci en juste <template #header>. Pensez-y comme "rendre ce fragment de template dans le slot 'header' du composant enfant".

named slots diagram

Voici le code transmettant le contenu des trois slots à <BaseLayout> en utilisant la syntaxe abrégée :

template
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

Lorsqu'un composant accepte à la fois un emplacement par défaut et des emplacements nommés, tous les nœuds de niveau supérieur non <template> sont implicitement traités comme du contenu pour le slot par défaut. Donc ce qui précède peut aussi s'écrire :

template
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- slot par défaut implicite -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

Désormais, tout ce qui se trouve à l'intérieur des éléments <template> sera transmis aux slots correspondants. Le rendu HTML final sera :

html
<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

Encore une fois, cela peut vous aider à mieux comprendre les slots nommés en utilisant l'analogie avec une fonction JavaScript :

js
// passage de plusieurs fragments de slot avec des noms différents
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> les affiche à différents emplacements
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

Slots conditionnels

Il arrive que l'on veuille rendre quelque chose en fonction de la présence ou non d'un slot.

Vous pouvez utiliser la propriété $slots en combinaison avec un v-if pour y parvenir.

Dans l'exemple ci-dessous, nous définissons un composant Card avec deux slots conditionnels : header et footer. Lorsque l'en-tête ou le pied de page sont présents, nous voulons les envelopper pour leur donner un style supplémentaire :

template
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>

    <div class="card-content">
      <slot />
    </div>

    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

Essayer en ligne

Noms de slot dynamiques

Les arguments de directive dynamique fonctionnent également sur v-slot, permettant la définition de noms de slots dynamiques :

template
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- avec syntaxe abrégée -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Notez que l'expression est soumise aux contraintes de syntaxe des arguments de directive dynamiques.

Scoped Slots

Comme indiqué dans Portée du rendu, le contenu du slot n'a pas accès à l'état dans le composant enfant.

Cependant, il existe des cas où il peut être utile que le contenu d'un slot puisse utiliser des données provenant à la fois de la portée du parent et de la portée de l'enfant. Pour y parvenir, nous avons besoin d'un moyen pour l'enfant de transmettre des données à un slot pour son affichage.

En fait, nous pouvons faire exactement cela - nous pouvons transmettre des attributs à un emplacement de slot comme on transmettrait des props à un composant :

template
<!-- template de <MyComponent> -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

La réception des props de slot est un peu différente lorsque vous utilisez un seul slot par défaut par rapport à l'utilisation de slots nommés. Nous allons d'abord montrer comment recevoir des props en utilisant un seul slot par défaut, en utilisant v-slot directement sur la balise du composant enfant :

template
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

scoped slots diagram

Les props passés au slot par l'enfant sont disponibles en tant que valeur de la directive v-slot correspondante, accessible par les expressions à l'intérieur du slot.

Vous pouvez considérer un "scoped slot" comme une fonction transmise au composant enfant. Le composant enfant l'appelle ensuite, en passant des props comme arguments :

js
MyComponent({
  // passage du slot par défaut, mais en tant que fonction
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return `<div>${
    // appel de la fonction slot avec les props !
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

En fait, c'est très proche de la façon dont les "scoped slots" sont compilés et de la façon dont vous utiliseriez les "scoped slots" dans les fonctions de rendu manuelles.

Remarquez comment v-slot="slotProps" correspond à la signature de la fonction slot. Tout comme avec les arguments de fonction, nous pouvons utiliser la déstructuration dans v-slot :

template
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

Scoped slots nommés

Les Scoped slots nommés fonctionnent de la même manière - les props du slot sont accessibles en tant que valeur de la directive v-slot : v-slot:name="slotProps". Lorsque vous utilisez le raccourci, cela ressemble à ceci :

template
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

Passer des props à un slot nommé :

template
<slot name="header" message="hello"></slot>

Notez que l'attribut name d'un slot ne sera pas inclus dans les props car il est réservé - donc le headerProps résultant serait { message: 'hello' }.

Si vous mélangez des slots nommés avec des "scoped slots" par défaut, vous devez utiliser une balise <template> explicite pour le slot par défaut. Tenter de placer la directive v-slot directement sur le composant entraînera une erreur de compilation. Ceci afin d'éviter toute ambiguïté sur la portée des props du slot par défaut. Par exemple :

template
<!-- Ce template ne compilera pas -->
<template>
  <MyComponent v-slot="{ message }">
    <p>{{ message }}</p>
    <template #footer>
      <!-- message appartient au slot par défaut, et n'est pas disponible ici -->
      <p>{{ message }}</p>
    </template>
  </MyComponent>
</template>

L'utilisation explicite d'une balise <template> pour le slot par défaut aide à indiquer clairement que la prop message n'est pas disponible dans l'autre slot :

template
<template>
  <MyComponent>
    <!-- Utiliser un slot par défaut explicit -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Here's some contact info</p>
    </template>
  </MyComponent>
</template>

Exemple Fancy List

Vous vous demandez peut-être quel serait un bon cas d'utilisation pour les scoped slots. Voici un exemple : imaginez un composant <FancyList> qui affiche une liste d'éléments - il peut encapsuler la logique de chargement des données distantes, utiliser les données pour afficher une liste, ou même des fonctionnalités avancées comme la pagination ou le défilement infini. Cependant, nous voulons qu'il soit flexible avec l'apparence de chaque élément et laisse la définition du style de chaque élément au composant parent qui le consomme. Ainsi, l'utilisation souhaitée peut ressembler à ceci :

template
<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

Dans <FancyList>, nous pouvons afficher le même <slot> plusieurs fois avec différentes données (notez que nous utilisons v-bind pour passer un objet en tant que props du slot) :

template
<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

Composants sans affichage

Le cas d'utilisation <FancyList> dont nous avons parlé ci-dessus encapsule à la fois la logique réutilisable (récupération de données, pagination, etc.) et l'affichage, tout en déléguant une partie de l'affichage au composant consommateur via des scoped slots.

Si nous poussons ce concept un peu plus loin, nous pouvons proposer des composants qui encapsulent uniquement la logique et n'affichent rien par eux-mêmes - l'affichage est entièrement délégué au composant consommateur avec des scoped slots. Nous appelons ce type de composant un Composant sans affichage (Renderless Component).

Un exemple de composant sans affichage pourrait être un composant qui encapsule la logique de suivi de la position actuelle de la souris :

template
<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

Bien qu'il s'agisse d'un pattern intéressant, la plupart de ce qui peut être réalisé avec les composants sans affichage peut être réalisé de manière plus efficace avec la Composition API, sans subir les coûts liés à l'imbrication de composants supplémentaires. Plus tard, nous verrons comment nous pouvons implémenter la même fonctionnalité de suivi de la souris mais avec un Composable.

Cela dit, les scoped slots sont toujours utiles dans les cas où nous devons à la fois encapsuler la logique et composer un affichage, comme dans l'exemple <FancyList>.

Slotsa chargé