Vue implements a content distribution API inspired by the Web Components spec draft, using the <slot>
element to serve as distribution outlets for content.
It promotes building composable and reusable components. Many folks have been writing about, or discussing, best practices, how to build components, how they communicate with each other, and how they should be structured in an app.
In this article, we'll dive head-first into Vue.js Slots, and Scoped Slots concepts, and go through code samples.
##Slots A Slot is a placeholder that allows content projection from a parent component to a child component.
The Vue engine, at runtime, processes the parent template, pulls the dynamic content, and injects it inside the Slot placeholder in the child component. In general, the child component hosts the <slot>
element, and the parent component specifies the content to go inside it.
Figure 1 shows the interaction between a parent and child component.
|
Figure 1: Parent and child component |
---|
Basically, Slots are used for building Layout components (also known as Master Pages), base components (Button, Hyperlink and the like) or any other type of composable components to provide flexibility for component consumers to inject dynamic content at runtime.
Let’s create a Button
component with a <slot>
element.
<template>
<button>
<slot>Click Me</slot>
</button>
</template>
The Button
component is basic and contains a single <button>
element. The button wraps a <slot>
component with a default fallback text. This means, if the parent component didn’t provide any content for the Slot, it will be replaced at runtime with the default fallback text Click Me.
Let's embed this Button
component inside a parent component.
<template>
<Button></Button>
</template>
The child component Button
is embedded into the parent component like any other normal component in Vue.js. Notice that in the code snippet above, the parent component is not injecting any content. When you run the app, you will see an HTML <button>
element with the default fallback text. Figure 2 shows the app running.
Figure 2: Slot with default content |
Let's inject some other text instead of the default fallback text, and run the app again. You have multiple choices when it comes to injecting content.
Without <template>
element
<Button>
Pretty Nice Button!
</Button>
With <template>
element
<Button>
<template v-slot:default>
Pretty Nice Button!
</template>
</Button>
Personally, I prefer the option using <template>
element. The template content will be rendered in place of the <slot>
element.
The <slot>
element has a special attribute called name
. This allows you to embed multiple Slots in your component, each designated with a different Slot name.
A <slot>
element without a name
attribute is considered the default Slot. To provide content for a <slot>
element, you would use the v-slot
directive, and then specify the name of the Slot.
In this code snippet, we could have removed the v-slot:default
as this is the only <slot>
element in the component. But, as a best practice, and in case you add more Slots in the future, I’d recommend you stick to this naming convention by using the v-slot:default
.
You can play with this example on codesandbox.io.
Let's assume the Button
component has another slot:
<template>
<button>
<slot>Click Me</slot>
<br>
<slot name="footer"/>
</button>
</template>
Inside the parent component:
<template>
<Button>
<template v-slot:default>Pretty Nice Button!</template>
<template v-slot:footer>Footer!</template>
</Button>
</template>
There are two <template>
elements, each for a specific slot.To add content to the default <slot>
element, you add the v-slot:default
directive by specifying the name of the slot. For the footer Slot, you provide the v-slot:footer
directive to target the footer <slot>
element.
Now, it makes more sense to use the v-slot
directive on both Slots to differentiate them, and make your code readable.
You can play with this example on codesandbox.io.
##Slots and Props
Thus far, we have seen how to use <slot>
elements, and inject content to them. In some other cases, you might need to pass data to Slots.
Slots are generally compiled under the parent’s component context. This means whatever content you project inside a <slot>
element is aware of the parent’s context only. For instance, if you have an interpolation or binding projected inside the <slot>
element, these will be compiled with respect to the parent component, and not the child component. A code snippet is worth a thousand words.
<template>
<Button>
<template v-slot:default>
{{ text }}
</template>
</Button>
</template>
<script>
import Button from "./components/Button";
export default {
components: {
Button
},
data() {
return {
text: 'Pretty Nice Button!',
};
},
};
</script>
The {{ text }}
interpolation is referencing a local data variable defined on the component named text
. The Vue.js engine compiles the projected content inside the parent component scope, and embeds the results into the child component.
You can play with this example on codesandbox.io.
A <slot>
element can also render default fallback text passed from the parent component to the child component. How? Nothing special really. It's similar to how you pass data to Vue components via props.
Let's introduce a new text
property on the Button
component:
<template>
<button>
<slot>{{ text }}</slot>
</button>
</template>
<script>
export default {
props: {
text: {
type: String,
default: "Click Me"
}
}
};
</script>
In the parent component:
<template>
<div id="app">
<h2>Vue Slots Demo</h2>
<Button v-bind:text="text"></Button>
</div>
</template>
<script>
import Button from "./components/Button";
export default {
components: {
Button
},
data() {
return {
text: "Pretty Nice Button!!!!"
};
}
};
</script>
You make use of Vue.js binding convention to bind the text
prop on Button
component to a local variable also named text
. That’s all!
You can play with this example on codesandbox.io.
You can even go wild and accept a prop of type Function
on the child component! The child component can share its context with the parent component. The parent component can then manipulate the child component context and return whatever content back to the child component.
The child component accepts a prop of type Function
:
<template>
<button>
<slot>{{ textFnc('Hi from child!') }}</slot>
</button>
</template>
<script>
export default {
props: {
textFnc: {
type: Function
}
}
};
</script>
The default fallback content inside the child component calls the prop Function
passing some random text.
The parent component defines the prop Function
as follows:
<template>
<div id="app">
<h2>Vue Slots Demo</h2>
<Button v-bind:textFnc="(prefix) => prefix + ' - ' + message"></Button>
</div>
</template>
<script>
import Button from "./components/Button";
export default {
components: {
Button
},
data() {
return {
message: "Pretty Nice Button!!!!"
};
}
};
</script>
The function textFunc()
executes under the parent component context. In this case, the function returns the text received from the child component, and appends it to a message that’s local to the parent component. The end result is shown in Figure 3.
Figure 3: Function prop |
You can play with this example on codesandbox.io.
The takeaway, in using functions as props, is the ability to share the child component’s context with the parent component.
##Scoped Slots Passing functions as props to a child component is one way of sharing context between child and parent components. However, this method is limited to text only. The function defined on the parent component can only return text.
What if you want the parent component to pass HTML content to the child component’s slot(s) and also make use of the child component context? This is where Scoped Slots play their part.
Pretty complicated right? Well, not as complicated as you might think. Let’s check it out together!
Let's build a List
component that owns an array of objects with a title
and description
properties.
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item">
<p>
{{ item['title'] }}
<br>
{{ item['description'] }}
</p>
</slot>
</li>
</ul>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
}
}
};
</script>
The component uses a <ul>
element to loop over the items in the array. An <li>
element is used to represent a single item. The code places a <slot>
element inside each and every <li>
element. The slot is bound to the single item object. In addition, the <slot>
element defines a default fallback content.
Let’s switch to the parent component:
<template>
<div id="app">
<h2>Vue Slots Demo</h2>
<List :items="items" />
</div>
</template>
<script>
import List from "./components/List";
export default {
components: {
List
},
data() {
return {
items: [
{
id: 1,
title: "Item #1",
description: "Item #1 description"
},
{
id: 2,
title: "Item #2",
description: "Item #2 description"
},
{
id: 3,
title: "Item #3",
description: "Item #3 description"
},
{
id: 4,
title: "Item #4",
description: "Item #4 description"
},
{
id: 5,
title: "Item #5",
description: "Item #5 description"
}
]
};
}
};
</script>
The component binds the items
array property to the List
component property items
. It uses the default fallback content coming from the child component.
Figure 4 shows the app running:
Figure 4: List component with default fallback content |
Let’s see how we can make use of the Slot binding.
As a refresher, the child component defines the following:
<slot :item="item">
<p>
{{ item['title'] }}
<br>
{{ item['description'] }}
</p>
</slot>
The <slot>
element binds the item
object on the Slot. It also provides a default fallback content. What if the parent component wants to replace this default content with a new content? That's easy!
Let's change the parent component as follows:
<template>
<List :items="items">
<template v-slot:default="prop">
<strong>{{ prop.item.title }}</strong>
</template>
</List>
</template>
The code snippet provides a <template>
with a v-slot:default
directive. The value given to the v-slot:default
directive is actually the directive value representing the object passed in by the <slot>
element defined inside the child component.
As a side note, I am writing another article, to be published very soon, exploring custom directives in Vue.js. Stay tuned!
Going back to the code snippet, the parent component now has access to the child component represented by the v-slot:default
directive value.
The <template>
element replaces the default fallback content by rendering just the title
field and making it bold.
Figure 5 shows the app running:
Figure 5: List component with custom content |
Scoped Slots is a powerful tool in Vue.js to allow you to build composable and reusable components.
##Slots in Vue 3 The content on Slots and Scoped Slots in this article applies to both Vue 2 and Vue 3. I am not aware of any change up until this moment. Things might change with the final release of Vue 3, but we’ll have to wait and see together!
##Conclusion Vue Slots feature gives developers the flexibility needed to build reusable and composable components in their applications.