Let’s go over the renderless slot pattern in Vue and see the problems that it can help solve.
Introduced with Vue.js 2.3.0, scoped slots have considerably improved component reusability. For example, the renderless component pattern emerged and solved the problem of providing reusable behavior and pluggable presentation.
Here we’ll see how to solve the opposite problem: providing reusable presentation and pluggable behavior.
This pattern applies for components that implement complex behavior and have customizable presentation.
To do so:
Let’s take an example: a component performing an Ajax request and having a slot to display the result. The component handles the Ajax request and the loading state while the default slot provides the presentation.
Here a simplified implementation:
<template>
<div>
<slot v-if="loading" name="loading">
<div>Loading ...</div>
</slot>
<slot v-else v-bind={data}>
</slot>
</div>
</template>
<script>
export default {
props: ["url"],
data: () => ({
loading: true,
data: null
}),
async created() {
this.data = await fetch(this.url);
this.loading = false;
}
};
</script>
Usage:
<lazy-loading url="https://server/api/data">
<template #default="{ data }">
<div>{{ data }}</div>
</template>
</lazy-loading>
For the original post about this pattern, check here.
What if the problem is the contrary: imagine the main feature of a component is its presentation. On the other hand, behaviors should be customizable.
Imagine you are creating a tree component based on SVG, like this one:
You want to provide the SVG display and behavior such as retracting node and text highlighting on click.
A problem arises when you decide to not hard-code these behaviors and let the user of the component free to override them.
A simple solution to expose these behaviors would be to add methods and events to the component.
You’ll end up with something like:
<script>
export default {
mounted() {
// pseudo code
nodes.on('click',(node) => this.$emit('click', node));
},
methods: {
expandNode(node) {
//...
},
retractNode(node) {
//...
},
highlightText(node) {
//...
},
}
};
</script>
To add behavior to the component, the consumer of the component will need to use a ref in a parent component, something like:
<template>
<tree ref="tree" @click="onClick"></tree>
</template>
<script>
export default {
methods: {
onClick(node) {
this.$refs.tree.retractNode(node);
}
}
};
</script>
This approach has several drawbacks:
Let’s see how renderless slots can solve these issues.
A behavior consists basically of proving a reaction to an event. So let’s create a slot that receives access to events and component methods:
<template>
<div>
<slot name="behavior" :on="on" :actions="actions">
</slot>
</div>
</template>
<script>
export default {
methods: {
expandNode(node) { },
retractNode(node) { },
//...
},
computed:{
actions() {
const {expandNode, retractNode} = this;
return {expandNode, retractNode};
},
on() {
return this.$on.bind(this);
}
}
};
</script>
The on
attribute is the $on
method of the parent component, so it’s possible to listen to all events.
Implementing a behavior can be done as a renderless component. Let’s write the expand-on-click component:
export default {
props: ['on','action']
render: () => null,
created() {
this.on("click", (node) => {
this.actions.expandNode(node);
});
}
};
Usage:
<tree>
<template #behavior="{ on, actions }">
<expand-on-click v-bind="{ on, actions }"/>
</template>
</tree>
The main advantages of this solution are:
For example, by declaring the graph component as:
<template>
<div>
<slot name="behavior" :on="on" :actions="actions">
<expand-on-click v-bind="{ on, actions }"/>
</slot>
</div>
</template>
Let’s consider a highlight-on-hover component:
export default {
props: ['on','action']
render: () => null,
created() {
this.on("hover", (node) => {
this.actions.highlight(node);
});
}
};
Overriding standard behavior:
<tree>
<template #behavior="{ on, actions }">
<highlight-on-hover v-bind="{ on, actions }"/>
</template>
</tree>
Let’s add two pre-defined behaviors:
<tree>
<template #behavior="{ on, actions }">
<expand-on-click v-bind="{ on, actions }"/>
<highlight-on-hover v-bind="{ on, actions }"/>
</template>
</tree>
Component as behavior are self-explanatory.
The on
attribute gives access to all the component events. New events are by default available for the slot.
Renderless slots present an interesting alternative to expose method and events in a component. They provide more readable and reusable code.
The code of the tree component implementing this pattern is available on github: Vue.D3.tree
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!