DEV Community

Roman Vladimirov
Roman Vladimirov

Posted on

5 Vue.js slot tricks to watch out for

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)