Components and scoped slots in Vue.js
I took me many examples and a lot of practice to understand slots.
Whether it’s simple slots, named slots or scoped slots, you will find that it’s a powerful feature in Vue. Let’s dive into it.
The challenge
At the end of last month, I attempted to solve a small project while learning Vue.js concepts.
The project required to build a photo slider that used 2 child components in the App.vue
. The SwiperSlide
below finds itself in a scoped slot.
The slot resides in the Swiper
component as you see with v-slot="{ item }"
.
<!-- App.vue template -->
<template>
<section>
<Swiper
:list="images"
:index="index"
v-slot="{ item }"
@change="index = $event"
>
<SwiperSlide v-bind="item" />
</Swiper>
</section>
</template>
The challenge that I struggled with was to implement the code so:
- the
App.vue
provided the markup (I didn’t have to changeApp.vue
) - the
SwiperSlide
child component would be and didn’t need modification:
1<!-- SwiperSlide.vue template -->
<template>
<img :src="image" :alt="`Image ${title}`" width="400" height="200" />
</template>
<script setup>
defineProps({
image: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
});
</script>
I couldn’t understand how I had to code the Swiper
component without touching App.vue
and SwiperSlide
component.
I read a few times this section of the documentation that described the use case.
How did I make it work
With the following, I completed the task:
<template>
<section class="container">
<Transition>
<ul ref="ulElement">
<li>
<slot :item="{ ...currentImage }"></slot>
</li>
</ul>
</Transition>
<button v-show="showPrev" class="prev-slide" @click="prevImage"><</button>
<button v-show="showNext" class="next-slide" @click="nextImage">></button>
</section>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
const props = defineProps({
list: { type: Array, required: true },
index: { type: Number, required: true },
});
const emits = defineEmits(["@change"]);
const currentImage = computed(() => props.list[props.index]);
const currentImageIndex = computed(() => props.index + 1);
const ulElement = ref(null);
const translate = ref(null);
const showPrev = computed(() => currentImageIndex.value > 1);
const showNext = computed(() => currentImageIndex.value < props.list.length);
const prevImage = () => {
emits("@change", props.index - 1);
translate.value = `-${ulElement.value.offsetWidth}px`;
};
const nextImage = () => {
emits("@change", props.index + 1);
translate.value = `${ulElement.value.offsetWidth}px`;
};
</script>
First, the prop name on the <slot>
element is key: it must match what the parent provides to the slot, in our case it is v-slot="{ item }"
. So the prop must be named :item
.
Second, the actual image object must be destructured so that the props definition in SwiperSlide
receive them individually.
If I’m mistaken somewhere, tell me so I can correct the wording, but after trying it out, I’m pretty sure that I now understand how scoped slots work.
Thanks for reading this article.