Slots, Scoped Slots, and Render Function
๐ Slots
๐ Scoped Slots
๐คฏ Render Function
๐คฏ Slots
๐คฏ Scoped Slots
๐ Render Function
I build interfaces on the web.
I'm on CodePen, Twitter and Instagram as @coltborg."I want to write code today that allows me to write less code tomorrow."
"Show examples of code I've written to help others build more reusable components."
Slots allow for fluid content
Scoped Slots allow for reusable functionality
Render function gives you the power of programmatic templating
(But can also be used for renderless components)
There are situations, where you really need the full programmatic power of JavaScript. Thatโs where you can use the render function, a closer-to-the-compiler alternative to templates.
๐ <template>
compiles to render functions under the hood.
โ๏ธ A way to write Vue with JSX
โ๏ธ Useful for functional (stateless) components.
<!-- AnchoredHeading.vue -->
<template>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</template>
// AnchoredHeading.vue
export default {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name
this.$slots.default // array of children
)
},
props: {
level: {
type: Number,
required: true
}
}
};
A renderless component
What if you want to componentise functionality and define the markup uniquely?
The render function needs to return a VNODE.
{
name: 'SomeComponent',
render() {
return createElement('h1', this.blogTitle);
},
}
{
name: 'SomeComponent',
render() {
return this.$slots.default[0];
},
}
{
name: 'SomeComponent',
render() {
return this.$scopedSlots.default({
childData: this.childData,
childMethod: this.childMethod,
});
},
}
Vue implements a content distribution API thatโs modeled after the current Web Components spec draft, using the
element to serve as distribution outlets for content.
Slots are useful when you want to inject content in a specific place of a component.
Using Slots
<!-- MyButton.vue -->
<template>
<button class="...">
<slot></slot>
</button>
</template>
<!-- Calling the component in a parent -->
<my-button>
Slot Text
</my-button>
Quick note, my-button
(kebab-case) and MyButton
(PascalCase) are both acceptable and are only truely different when using non-string templates.
Using Default Content
<!-- MyAlert.vue -->
<div class="..." role="alert">
<slot></slot>
</div>
<!-- MyAlert.vue -->
<div class="..." role="alert">
- <slot></slot>
+ <slot>
+ <p class="...">General Warning</p>
+ <p>Something not ideal is happening.</p>
+ </slot>
</div>
Using Named Slots
<!-- MyInput.vue -->
<template>
<div>
<label class="...">
<div>Label</div>
<input :value="value" @input="emitInput" type="text" class="...">
</label>
<p class="...">A warning message.</p>
</div>
</template>
<!-- MyInput.vue -->
<template>
<div>
<label class="...">
- <div>Label</div>
+ <slot></slot>
<input :value="value" @input="emitInput" type="text" class="...">
</label>
- <p class="...">A warning message.</p>
+ <slot></slot>
</div>
</template>
<!-- MyInput.vue -->
<template>
<div>
<label class="...">
<slot></slot> ๐ค
<input :value="value" @input="emitInput" type="text" class="...">
</label>
<slot></slot> ๐ค
</div>
</template>
<!-- Parent Component -->
<template>
<my-input>
Test Label
</my-input>
</template>
<!-- MyInput.vue -->
<template>
<div>
<label class="...">
- <slot>Label</slot>
+ <slot name="label">Label</slot>
<input :value="value" @input="emitInput" type="text" class="...">
</label>
- <slot></slot>
+ <slot name="warning"></slot>
</div>
</template>
// MyTransition.vue
<template>
<transition name="slide-fade">
<slot></slot>
</transition>
</template>
<script>
export default {
name: 'SlideFade',
};
</script>
/* MyTransition.vue */
<style scoped="">
.slide-fade-leave-active {
transition: transform 300ms ease-in-out, opacity 150ms ease-in-out;
}
.slide-fade-enter {
opacity: 0;
}
.slide-fade-leave-to {
transform: translateY(-1rem);
opacity: 0;
}
</style>
<my-transition>
<any-component v-if="isShown"></any-component>
</my-transition>
Sometimes youโll want to provide a component with a reusable slot that can access data from the child component.
When you want a template inside of a slot to access data (or methods) from the child component.
With Scoped Slot Data
<!-- MyList.vue -->
<template>
<div>
<slot v-for="item in items" :item="item">
</slot>
</div>
</template>
<script>
export default {
name: 'MyList',
props: {
items: {
type: Array,
default: () => [],
},
},
};
</script>
<!-- Parent -->
<template>
<my-list :items="listItems">
<div slot-scope="slotProps" class="...">
{{ slotProps.item.text }}
</div>
</my-list>
</template>
<script>
export default {
name: 'ParentComponent',
data() {
return {
listItems: [
{ text: 'Ice cream', emoji: '๐ฆ' },
{ text: 'Magic', emoji: '๐ง๐ปโโ๏ธ' },
{ text: 'Space', emoji: '๐' },
]
};
}
};
</script>
<!-- Parent -->
<template>
<my-list :items="listItems">
- <div slot-scope="slotProps" class="...">
+ <div slot-scope="{ item }" class="...">
- {{ slotProps.item.text }}
+ {{ item.text }}
</div>
</div></my-list></template>
Encapsulating 3rd Party JavaScript
<script>
import Popper from 'popper.js';
export default {
name: 'Popper',
props: {
config: {
type: Object,
default: () => ({ placement: 'bottom' }),
},
},
data() {
return {};
},
render() {
return this.$scopedSlots.default({});
},
};
</script>
...
data() {
- return {};
+ return {
+ isOpen: false,
+ triggerEl: null,
+ popperEl: null,
+ };
},
+ mounted() {
+ this.triggerEl = this.$el.querySelector('[data-trigger]');
+ this.popperEl = this.$el.querySelector('[data-popper]');
+ },
...
// ...
{
methods: {
setupPopper() {
if (this.popper === undefined) {
this.popper = new Popper(this.triggerEl, this.popperEl, this.config);
} else {
this.popper.scheduleUpdate();
}
},
},
}
// ...
// ...
{
methods: {
// ...
open() {
if (this.isOpen) {
return;
}
this.isOpen = true;
this.$nextTick(() => {
this.setupPopper();
});
},
close() {
if (!this.isOpen) {
return;
}
this.isOpen = false;
},
},
}
// ...
// ...
{
data() {
// ...
},
beforeDestroy() {
if (!this.popper) {
return;
}
this.popper.destroy();
},
mounted() {
// ...
},
// ...
}
// ...
// ...
{
// ...
render() {
- return this.$scopedSlots.default({});
+ return this.$scopedSlots.default({
+ isOpen: this.isOpen,
+ open: this.open,
+ close: this.close,
+ });
},
}
<!-- Parent Component -->
<template>
<my-popper>
<div slot-scope="{ isOpen, open, close }">
<button data-trigger="">Action</button>
<div v-show="isOpen" data-popper="">
Content of the popper goes here
</div>
</div>
</my-popper>
</template>
<script>
import MyPopper from '@/components';
export default {
name: 'ComponentUsingPopper',
components: {
MyPopper,
},
}
</script>
Components In Components In Components
(Cue Inception Horns)
<!-- (Summary of the code) -->
<template>
<my-popper>
<div slot-scope="{ isOpen, open, close }" @mouseover="open" @mouseleave="close">
<!-- SVG trigger -->
<my-transition>
<div v-show="isOpen">
<!-- Popper content -->
</div>
</my-transition>
</div>
</my-popper>
</template>
<!-- (Summary of the code) -->
<template>
<my-popper>
<div slot-scope="{ isOpen, open, close }">
<myinput @input="validate($event, open, close)">
<my-transition>
<div v-show="isOpen">
<!-- Popper content -->
</div>
</my-transition>
</myinput></div>
</my-popper>
</template>
<!-- (Summary of the code) -->
<template>
<my-popper>
<on-outside-click slot-scope="{ isOpen, open, close }" :do="close">
<div class="flex">
<my-button @click.native="open" @keydown.esc="close">
<!-- Button content -->
</my-button>
<my-transition>
<div v-show="isOpen">
<!-- Menu content -->
</div>
</my-transition>
</div>
</on-outside-click>
</my-popper>
</template>