frontend / vuejs
Composants & slots
Explication
Ce que vous allez apprendre
- Créer un slot par défaut et des slots nommés pour des zones de contenu multiples
- Fournir un contenu de secours affiché quand le parent ne personnalise pas un slot
- Comprendre comment un scoped slot inverse le flux de données habituel (enfant vers parent)
- Utiliser
<component :is="...">pour changer de composant dynamiquement - Préserver l'état de composants inactifs avec
<KeepAlive>
Dans quel contexte ?
Un composant Carte générique doit s'adapter à des besoins très différents selon l'endroit où il est utilisé : parfois avec un simple texte, parfois avec un titre personnalisé et un pied de page avec des actions. Plutôt que de multiplier les props pour chaque variation possible, les slots permettent au parent d'injecter directement du contenu HTML arbitraire à des emplacements précis du composant enfant.
D'abord, un emplacement où injecter du contenu
<slot /> (sans nom) marque l'endroit où le contenu placé entre les balises ouvrante et fermante du composant sera inséré. C'est le mécanisme le plus simple : le parent écrit du HTML normal à l'intérieur de <Carte>...</Carte>, et ce contenu apparaît exactement là où <slot /> est déclaré dans le composant enfant.
Un <slot name="entete"> nommé permet de cibler plusieurs zones distinctes du même composant, chacune remplie indépendamment par le parent via <template #entete>.
Prérequis
Il faut avoir compris les props (leçon 5) : les slots complètent les props en permettant de transmettre du contenu HTML, là où les props ne transmettent que des données.
Ensuite, un contenu de secours qui simplifie l'usage
Le contenu placé DIRECTEMENT à l'intérieur d'une balise <slot> (comme <h3>Titre par défaut</h3> dans l'exemple) s'affiche uniquement si le parent ne fournit rien pour ce slot précis. Cette valeur par défaut rend le composant utilisable immédiatement, sans forcer chaque appelant à tout personnaliser.
| Type de slot | Rôle | Syntaxe côté parent |
|---|---|---|
| Slot par défaut | Contenu principal du composant | Contenu direct entre les balises |
| Slot nommé | Zone spécifique (en-tête, pied de page) | <template #nom> |
| Scoped slot | L'enfant transmet des données au contenu du parent | v-slot="{ donnee }" |
Il reste un mécanisme plus avancé : le flux inversé
Un scoped slot inverse la direction habituelle : au lieu que le parent envoie seulement du contenu à l'enfant, l'enfant transmet des DONNÉES au template fourni par le parent. <slot :utilisateur="u" :est-admin="..." /> expose ces valeurs, que le parent récupère avec v-slot="{ utilisateur, estAdmin }".
Astuce
Les scoped slots sont particulièrement utiles pour des composants de type "liste" ou "tableau" : le composant enfant gère la boucle et la logique de récupération des données, mais laisse au parent le contrôle total de l'affichage de CHAQUE élément individuel.
Maintenant, changer de composant à la volée
<component :is="ongletActif"> permet de basculer dynamiquement entre plusieurs composants selon une valeur réactive — le pattern classique d'un système d'onglets. Sans précaution, chaque changement d'onglet détruirait et recréerait le composant depuis zéro, perdant tout état interne (comme un formulaire à moitié rempli).
Piège fréquent
Utiliser <component :is="..."> sans <KeepAlive> autour force Vue à démonter puis remonter entièrement le composant à chaque changement, ce qui réinitialise systématiquement son état interne. Si l'utilisateur doit pouvoir revenir sur un onglet sans perdre sa saisie, <KeepAlive> est indispensable.
Maintenant que tu sais transmettre du contenu et préserver l'état entre composants, la prochaine leçon aborde un autre mécanisme de communication : partager des données entre des composants très éloignés dans l'arbre, sans les faire transiter manuellement à travers chaque niveau intermédiaire.
Commandes & code
Composants & slots
Les slots permettent à un composant parent d'injecter du contenu (voire du contenu paramétré) dans un composant enfant.
<!-- components/Carte.vue -->
<template>
<div class="carte">
<header class="carte-entete">
<!-- slot nommé : le parent cible cet emplacement via #entete -->
<slot name="entete">
<!-- contenu par défaut si le parent ne fournit rien -->
<h3>Titre par défaut</h3>
</slot>
</header>
<div class="carte-corps">
<!-- slot par défaut (sans nom) -->
<slot />
</div>
<footer class="carte-pied" v-if="$slots.pied">
<slot name="pied" />
</footer>
</div>
</template><!-- Utilisation avec la syntaxe complète <template #nom> -->
<script setup>
import Carte from './components/Carte.vue'
</script>
<template>
<Carte>
<template #entete>
<h3>Statistiques du mois</h3>
</template>
<!-- Le contenu sans balise <template> va automatiquement dans le slot par défaut -->
<p>127 nouveaux utilisateurs, +12% vs le mois dernier.</p>
<template #pied>
<button>Voir le rapport complet</button>
</template>
</Carte>
</template><!-- Scoped slots : le composant ENFANT transmet des données au contenu fourni par le parent -->
<!-- components/ListeUtilisateurs.vue -->
<script setup>
defineProps({ utilisateurs: Array })
</script>
<template>
<ul>
<li v-for="u in utilisateurs" :key="u.id">
<!-- on expose `utilisateur` et `estAdmin` au parent via le slot -->
<slot :utilisateur="u" :est-admin="u.role === 'admin'">
<!-- rendu par défaut si le parent ne personnalise pas -->
{{ u.nom }}
</slot>
</li>
</ul>
</template><script setup>
import ListeUtilisateurs from './components/ListeUtilisateurs.vue'
const utilisateurs = [
{ id: 1, nom: 'Alice', role: 'admin' },
{ id: 2, nom: 'Bob', role: 'membre' },
]
</script>
<template>
<!-- v-slot déstructure directement les props exposées par le slot enfant -->
<ListeUtilisateurs :utilisateurs="utilisateurs" v-slot="{ utilisateur, estAdmin }">
<strong :class="{ 'texte-admin': estAdmin }">{{ utilisateur.nom }}</strong>
<span v-if="estAdmin"> (admin)</span>
</ListeUtilisateurs>
</template><!-- Composants dynamiques : <component :is="..."> change de composant à la volée -->
<script setup>
import { ref, shallowRef } from 'vue'
import OngletProfil from './components/OngletProfil.vue'
import OngletSecurite from './components/OngletSecurite.vue'
import OngletFacturation from './components/OngletFacturation.vue'
// shallowRef : pas besoin de réactivité profonde sur une référence de composant
const ongletActif = shallowRef(OngletProfil)
const composants = { profil: OngletProfil, securite: OngletSecurite, facturation: OngletFacturation }
</script>
<template>
<nav>
<button v-for="(comp, cle) in composants" :key="cle" @click="ongletActif = comp">
{{ cle }}
</button>
</nav>
<!-- KeepAlive : préserve l'état des composants inactifs (formulaire non perdu en changeant d'onglet) -->
<KeepAlive>
<component :is="ongletActif" />
</KeepAlive>
</template>Résumé
- Slot par défaut pour du contenu simple, slots nommés (
#nom) pour des zones multiples. - Les scoped slots inversent le flux : l'enfant transmet des données au template fourni par le parent.
<component :is="...">+<KeepAlive>est le pattern standard pour des systèmes d'onglets performants.- Toujours prévoir un contenu par défaut dans un slot quand c'est pertinent (composant plus robuste).
Exercices pratiques
Mission : un système d'onglets qui oublie tout
Objectif : Diagnostiquer la perte d'état d'un composant dynamique entre deux onglets, puis exploiter un scoped slot pour un composant de liste réutilisable.
Contexte
Une application affiche trois onglets (Profil, Sécurité, Facturation) via <component :is="ongletActif" />, sans <KeepAlive> autour. L'onglet Sécurité contient un formulaire de changement de mot de passe à trois champs. Un testeur signale que dès qu'il change d'onglet puis revient sur Sécurité, tous les champs qu'il avait commencé à remplir sont vides à nouveau.