In the last part of this article, we looked at the most basic form of Vue’s <slot>
. We learned how to create basic components that allow passing any sort of structure and data into them, and we took a look at how to create multi-slot components.
This time, we are going to look at the basic <slot>
's amped-up sister, the scoped slot.
Imagine that you are building a Pokemon card game, and you want to have a <Card>
component that has some default slots for what is being displayed in the card. But you also want to give control to the parent of the information that is rendered in this space, for example, on the main content area of the card.
You may be thinking, easy, I just set a default content inside the slot in <Card>
, and then override it on the parent, which is exactly where I want your mindset to be—Pokemon. You are stuck inside a v-for loop through an array of data. How are you going to handle an event that changes that default content? Are you going to capture the current Pokemon in the loop and store it in a variable? Pass it to a method?
Scoped slots allow us to expose a piece of data, through the scope, to the parent that is using it. Imagine the following:
<Card>
component and you give it a pokemon
internal state.<Card>
makes a random call to the API and fetches a Pokemon for itself.<Card>
exposes a name slot that is being defaulted to the Pokemon’s name.pokemon
, your parent can grab it and use it as needed.In order to better understand how scoped slot works, let’s create the card in the above example. We are going to use the Pokemon API for this!
We are going to create a better named card called <PokeCard>
. The basic code for it will be the following.
<template>
<div>{{ pokemon.name }}</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
pokemon: null
};
},
created() {
axios.get(
`https://pokeapi.co/api/v2/pokemon/${Math.round(Math.random() * 150)}`
).then(result => {
this.pokemon = result.data;
});
}
};
</script>
We are importing Axios
because we are going to use it as our go-to library to make the async call to the API endpoint.
Next, in the created
method, we use Axios’ get
method to make a call to the endpoint of the PokeAPI that will return a Pokemon’s data. If you want to look at the documentation for this endpoint, you can visit the official page here.
This get
method for Axios returns a JavaScript Promise
. I’m not going to go into depth on how these work, but if you want to freshen up, here’s the link to the MDN page on Promises.
In the then
block of the Promise, we are capturing the result of the call. Note that axios
will wrap this result in an object of its own, so we need to access the information through the data
property. This, in return, will hold the information that the API is giving us—that is, the actual Pokemon’s data.
Finally, we are simply dumping the [pokemon.name](http://pokemon.name)
into view for now.
Go to your App.vue
or wherever you are going to render this, and let’s create a loop to showcase the Card.
<template>
<div id="app">
<PokeCard :key="i" v-for="i in 20"/>
</div>
</template>
<script>
import PokeCard from "./components/PokeCard";
export default {
name: "App",
components: {
PokeCard
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Don’t forget to add the :key
attribute! If you want a refresher on what key
is and why it’s super important, you can check out my article on key
here.
The v-for
loop in the previous example will render 20 different <PokeCard>
s in the screen, feel free to adjust as needed. Once you load this into your browser, you should see 20 Pokemon names pop up. Neat!
I say “pretty” in between quotes because my design skills are about as good as my cooking. Proceed at your own risk, and order pizza.
After some fiddling, I came up with the following for our beautiful PokeCard
. Feel free to make this a work of art and show me how it’s done at @marinamosti. :D
<template>
<div class="card">
<div class="name">{{ pokemon.name }}</div>
<div>
<img :src="pokemon.sprites.front_default">
</div>
<div class="types">
<ul>
<li v-for="type in pokemon.types" :key="type.slot">{{ type.type.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
pokemon: null
};
},
created() {
axios
.get(
`https://pokeapi.co/api/v2/pokemon/${Math.round(Math.random() * 150)}`
)
.then(result => {
this.pokemon = result.data;
});
}
};
</script>
<style lang="scss" scoped>
.card {
border: 1px solid black;
border-radius: 10px;
margin: 0 auto;
margin-bottom: 2rem;
display: inline-block;
.name {
text-transform: capitalize;
padding: 2rem 0;
}
.types {
ul {
margin: 0;
padding: 0;
list-style: none;
}
}
}
</style>
I’ve added some <style>
s to the card, and in the template some markup to display the image and types for our Pokemon.
Time to start scoping this! Let’s add a regular named slot as we saw in the last article first. I want to keep the name and the image intact, but give the user of the component the ability to modify the contents of what is showing below the image.
<template>
<div class="card">
<div class="name">{{ pokemon.name }}</div>
<div>
<img :src="pokemon.sprites.front_default">
</div>
<slot name="content">
<div class="types">
<ul>
<li v-for="type in pokemon.types" :key="type.slot">{{ type.type.name }}</li>
</ul>
</div>
</slot>
</div>
</template>
I’ve wrapped the div.types
content all with a named <slot>
called content
. This will allow for all this part to be overwritten by the parent.
Let’s go back to App.vue
(or wherever you are rending this list) and make a small adjustment so that every “odd” card has the content replaced.
<PokeCard :key="i" v-for="i in 20">
<template v-slot:content v-if="i % 2">
This is a normal slot.<br/>How do I get the data?
</template>
</PokeCard>
Sweet! We’ve added a <template>
that declares a v-slot:
with the name content
, so anything we put in here is going to overwrite what we currently have as the “types” list.
Now, I want to be able to overwrite this in the parent as a list of the Pokemon’s moves! Except … how? The data for the Pokemon is inside the card. 🤔
For cases such as these where we need to expose a piece of data from the child to the parent through a slot, we have what are called scoped slots
. I’ve seen a lot of people struggling with this concept, so hopefully with this very simple and dumb example you will be able to grasp the concept, since technically it won’t be challenging to do!
We need to expose
or bind
the pokemon
property to this slot first, so that it is “shown” to the parent.
<slot name="content" v-bind:pokemon="pokemon">
[...]
</slot>
Update your <slot>
inside PokeCard.vue
to v-bind:pokemon
to the pokemon
internal state. You can also use the short syntax :pokemon="pokemon"
.
What this is doing is literally binding that data into the slot. Think about the slot as a box, and right now we are putting these variables in the box. Whoever wishes to use this box (the parent) can make use of these internal variables!
Now head over to App.vue
and let’s make some small adjustments.
<PokeCard :key="i" v-for="i in 20">
<template v-slot:content="props" v-if="i % 2">
{{ props.pokemon.name }}
</template>
</PokeCard>
I’ve gone ahead and added a little bit of syntax to the v-slot:content
declaration. You can see that it now has a second part ="props"
. What exactly does this mean?
What is means, literally, is:
“This slot (v-slot
) named content (:content
) will receive an object named props (="props"
) with some data that you can use.”
Now, check the line that follows inside the <template>
. We are accessing the name
of the Pokemon by first looking inside the props
object, then inside the pokemon
property of this object, finally we find the name
and display it inside the template.
What can you find inside this object you ask? Anything and everything your component declared as a binding inside the <slot>
! Remember when we did this?
<slot name="content" v-bind:pokemon="pokemon">
Well, that :pokemon="pokemon"
is EXACTLY what you’re getting inside the props.pokemon
object!
One more thing is left for our neat example. Right now we are only displaying the name
of the Pokemon in the scoped slot, but we said earlier we wanted to show all the moves that it has instead of its types.
Let’s make some changes to our App.vue
inside the v-slot:content
declaration that lives within our <PokeCard>
.
<PokeCard :key="i" v-for="i in 20">
<template v-slot:content="props" v-if="i % 2">
<ul style="margin: 0; padding: 0; list-style: none;">
<li v-for="move in props.pokemon.moves.slice(0,3)"
:key="move.slot">
{{ move.move.name }}
</li>
</ul>
</template>
</PokeCard>
A couple of noteworthy things. The v-if
declaration here is making it so that we only display this template on odd cards (1, 3, 5, etc.).
The <li>
has a v-for
in which we’re looping through the props.pokemon.moves
object, but I’ve appended slice(0,3)
to keep the array to a maximum of 3 items. Some of these little guys can learn a LOT of moves.
Finally we display the move
's name inside the <li>
. Go ahead into your browser and behold the awesome!
One last thing I want to mention before wrapping up the taco.
You may have seen in others’ code or articles that the v-slot
for scoped slot syntax involves curly braces, like so.
<template v-slot:content="{pokemon}">
I didn’t want to confuse you earlier, so I left this little bit out. This is not special Vue syntax or magic, this is object destructuring. What is happening here is that inside the props
object that we had before, we have a pokemon
property, right?
Well, we are simply telling JavaScript to extract that property so we can use it directly. So instead of props.pokemon.moves
, you would write pokemon.moves
. Handy!
Object destructuring though is out of the scope of this article, so I won’t go into further detail.
The code for this article can be found in the following codesandbox:
https://codesandbox.io/s/pokecards-hnbph
Scoped slots is one of those things that can take a bit to wrap your head around, but once you Catch 'em, its a very powerful tool in your arsenal!
As always, thanks for reading and share with me your scoped slot adventures and favorite Pokemon on Twitter at: @marinamosti .
PS. All hail the magical avocado! 🥑
P.P.S. ❤️🔥🐶☠️
Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.