Vue.js is a powerful library for creating user interfaces for web and mobile platforms. It is part of the so-called big three (also Angular and React) libraries for interactive user interface programming. I think you use slots all the time when using Vue.js. It's so simple and intuitive mechanism that it is a pleasure to use it. Slots are everywhere, in libraries, in components, and in scaffolders. But for the most part, everyone uses them only on one side, on the side of consumption, and when creating their components, they are rarely added. In this article I will show you how easy it is to add slots into your own components and a few cases where they can save a couple of lines of code.
Preface
First, what is a slot in Vue.js. A slot is a special communication mechanism between components. Yes, you should think about slots
not as a mechanism for inserting content, but as a mechanism for communication between components. So, in all examples, we will have two components TestParent.vue
and TestChildren.vue
. I will not use TypeScript
and Composition API
to keep all examples as simple as possible.
The simplest example of communication through slots:
<!-- TestParent.vue -->
<template>
<div>
<test-children>
<span>World</span>
</test-children>
</div>
</template>
<script>
export default function () {
const TestChildren = <...>;
return {
name: `Parent`,
components: { TestChildren }
}
}
</script>
<!-- TestChildren.vue -->
<template>
<div>
<span>Hello</span>
<slot></slot>
</div>
</template>
As a result, we will get the text Hello World
.
In our example, I used the so-called default slot. You can define more slots, for example let's add another slot to indicate an exclamation point.
<!-- TestParent.vue -->
<template>
<div>
<test-children>
<span>World</span>
<template v-slot:extra>
!
</template
</test-children>
</div>
</template>
<!--
I omitted the script section because it
hasn't changed from the previous example
-->
<!-- TestChildren.vue -->
<template>
<div>
<span>Hello</span>
<slot></slot>
<slot name="extra"></slot>
</div>
</template>
The result is Hello World !
. Here we have added a named slot called extra
where we added an exclamation mark. You can add as many named slots as you want to your component. You can even create them dynamically.
So, we are ready to move on to cases that can be useful in the daily creation of components.
Case 1: Consolidation of functionality
Suppose the TestChildren
component displays text with formatting that removes edge spaces (trim operation). It also has an additional slot for displaying additional text. We add additional text in the TestParent
component.
<!-- TestParent.vue -->
<template>
<div>
<test-children header="Hello">
<template v-slot:extra>
{{ parentValue }}
</template
</test-children>
</div>
</template>
<script>
export default function () {
const TestChildren = <...>;
return {
name: `Parent`,
components: { TestChildren },
data() {
return {
parentValue: 'World!'
}
}
}
}
</script>
<!-- TestChildren.vue -->
<template>
<div>
<!-- Using a formatting method -->
<span>{{ formatValue(header) }}</span>
<slot name="extra"></slot>
</div>
</template>
<script>
export default function () {
return {
name: `TestChildren`,
props: {
header: String
},
methods: {
// we have added a new formatting method
formatValue(value) {
return value.trim()
}
}
}
}
</script>
All goes well until we get a request to also format the parentValue
in the TestParent
. The problem here is that the formatting function is in the TestChildren
component. You can naturally take out the formatValue
function from the component into a separate js / ts file and import it to both components. But what if I will say that you can do this without separating the function into a separate script file. You can use the functionality of the slots to solve this problem. Let's change it so that we can use this function from the TestParent
component.
<!-- TestParent.vue -->
<template>
<div>
<test-children header="Hello">
<template v-slot:extra="{ format }">
<!-- call the method to format the value -->
{{ format(parentValue) }}
</template
</test-children>
</div>
</template>
<!-- script section stay as in previous example -->
<!-- TestChildren.vue -->
<template>
<div>
<span>{{ formatValue(header) }}</span>
<slot name="extra" :format="formatValue"></slot>
</div>
</template>
<script>
export default function () {
return {
name: `TestChildren`,
props: {
header: String
},
methods: {
formatValue(value) {
return value.trim()
}
}
}
}
</script>
As we see, we got result without using extra files just inside our components. The TestChildren
component exposes the function for the TestParent
component only in the slot definition.
Case 2: Component without UI
This case is an extension of the previous case to the entire component. Let's say we have a small (or maybe vice versa large) component, but each time it is used, a different representation must be used. A simple example is a button, a button can be in the form of a rectangle and as a link, as an icon, etc. How to solve this problem? We can create a component in which we describe the logic of how it should work (methods and properties) and when using an external component, it will decide which view it should use.
<!-- TestParent.vue -->
<template>
<div>
<test-children
title="Say Hello World!"
@clicked="fire()">
<template #default="{ context }">
<!--
We use context to create a view for the button.
-->
<button
@click="context.click($event)">
<span>
{{ context.title }}
</span>
</button>
</template>
</test-children>
</div>
</template>
<script>
export default function () {
const TestChildren = <...>;
return {
name: `Parent`,
components: { TestChildren },
data() {
return {
}
},
methods: {
fire() {
window.alert('Hello World!');
}
}
}
}
</script>
<!-- TestChildren.vue -->
<template>
<div>
<!-- We put the whole component in the context variable -->
<slot :context="this"></slot>
</div>
</template>
<script>
export default {
name: `TestChildren`,
props: {
title: {
type: String,
default: () => ``
},
disable: {
type: Boolean,
default: () => false
}
},
emits: [`clicked`],
methods: {
click($event) {
if (this.disable) return;
this.$emit(`clicked`, $event);
}
}
};
</script>
The TestChildren
component contains a set of properties (title
and disable
) and a click
method. This state and behavior is the same in all buttons regardless of how they look. In this way, we can save code by not creating the same type of components that just look different but have the same functionality.
Case 3: Rollover slots
Let's say we have three components, first SomeComponent
in which there are several slots (among which there are header
and footer
).
It is imported within second TestChildren
component and it defines the content for all the slots of the SomeComponent
component, plus some functionality. And we have third TestParent
component where imported TestChildren
. At some point in time, there is a need to change the content for slots in different situations, but only for header
and footer
everything else remains as is. I have a component (perhaps complex) but I need to somehow definition content for slot from the parent component for the slot of the component nested in different component. Let's take an example, in the TestParent
component, I should be able to change the content of the footer
slot in the SomeComponent
component, which is imported within TestChildren
component.
<!-- SomeComponent.vue -->
<template>
<div>
<slot name="header" :message="'value is '"></slot>
<slot name="footer" :value="1"></slot>
</div>
</template>
<!-- TestChildren.vue -->
<template>
<some-component>
<!-- Here we rollover the header and footer slots -->
<template v-slot:header="{ message }">
<slot name="header" :message="message"></slot>
</template>
<template v-slot:footer="{ value }">
<slot name="footer" :value="value"></slot>
</template>
<!-- definition for other slots and other stuff -->
</some-component>
</template>
<!-- TestParent.vue -->
<template>
<div>
<test-children>
<template v-slot:header="{ message }">
<span style="color: red;">{{ message }}</span>
</template>
<template v-slot:footer="{ value }">
<span style="color: green;">{{ value }}</span>
</template>
</test-children>
</div>
</template>
As a result, we get value is 1
but written in different colors. I draw your attention to the fact that in addition to the slots themselves, I pass the message
and value
parameters for the slots. This approach allows you to rollover some slots for the parent component from other the component, increasing the customization of the all components.
Case 4: Data change chain
I am using the same components as in the previous example to illustrate the idea. In the previous example, we just rollover from one slot to another the data of the header
and footer
slots, but what if I tell you that we can change them during the rollover.
<!-- SomeComponent.vue -->
<template>
<div>
<slot name="header" :message="'value is '"></slot>
<slot name="footer" :value="1"></slot>
</div>
</template>
<!-- TestChildren.vue -->
<template>
<some-component>
<template v-slot:header="{ message }">
<!-- transform message to upper case -->
<slot
name="header"
:message="message.toUpperCase()">
</slot>
</template>
<template v-slot:footer="{ value }">
<!-- multiply on ten -->
<slot name="footer" :value="value * 10"></slot>
</template>
<!-- definition for other slots and other stuff -->
</some-component>
</template>
<!-- TestParent.vue -->
<template>
<div>
<test-children>
<template v-slot:header="{ message }">
<span>{{ message + ":" }}</span>
</template>
<template v-slot:footer="{ value }">
<span>{{ value / 10 }}</span>
</template>
</test-children>
</div>
</template>
Result: VALUE: 1
. Although this example seems to be extremely simple, imagine what can be done based on this idea. For example, destructuring a large object into small ones or vice versa. And note that the scope of such transformations does not go beyond the components, each component is have its own responsibility and knows nothing about data transformations in other components.
Case 5: Combining slots
Let's say we have some component SomeComponent
and it has two slots - header
, footer
. Sometimes it happens that both of these slots must contain the same content. To solve this problem, we will make a TestChildren
component which will imported SomeComponent
but define only one postcontent
slot and insert it into the header
and footer
slots.
<!-- TestParent.vue -->
<template>
<div>
<test-children>
<template v-slot:postcontent>
<span>Header and Footer Message</span>
</template>
</test-children>
</div>
</template>
<!-- TestChildren.vue -->
<template>
<some-component>
<!-- insert the postcontent slot definition into the header and footer slots -->
<template v-slot:header>
<slot name="postcontent"></slot>
</template>
<template v-slot:footer>
<slot name="postcontent"></slot>
</template>
</some-component>
</template>
Thus, from the point of view of the TestParent
component, there is only one slot, which is actually multiplied by several places. Although this case seems highly unlikely, if you take a close look at your components, you will most likely find similar cases. For example, the title text of a blog post is repeated at the beginning of the article, in the header, in some kind of drop-down panel, etc.
Afterword
I hope you find my tricks for Vue.js slots was interesting and will be useful for someone.
Top comments (0)