In the last days, I built my first Vue component using a render
function instead of the common <template>
tag.
Now, my impostor syndrome tells me that the things I've learned while doing so are totally obvious to anyone else using Vue.
Maybe that's the case – and maybe it isn't. I hope you can take something away from this story. Or tell me where I've overseen obvious flaws in the solution 🙈
Why did I use the render function?
What I've build is a tabs component. I had a look at different existing solutions for that. I also talked with colleagues about a nice interface for such a component. We all liked the way Vuetify handles the case. You just throw in some <Tab>
s and the same number of <TabItem>
s and the <Tabs>
component magically takes care of toggling the content and active states:
<AwesomeTabs>
<MyTab>Tab #1</MyTab>
<MyTab>Tab #2</MyTab>
<MyTab>Tab #3</MyTab>
<MyTabContent>Content #1</MyTabContent>
<MyTabContent>Content #2</MyTabContent>
<MyTabContent>Content #3</MyTabContent>
</AwesomeTabs>
With a structure like this, you can't simply throw everything into the default
slot of a Vue template
. You do not want to render the <Tab>
s and <TabItem>
s next to each other. Instead, this needs some logic to toggle an active
state for the currently selected <Tab>
and only show the currently selected <TabItem>
.
How the render function works
Of course, you should check the Vue documentation on render functions. Quick TL;DR here:
- The
render
function returns whatever you want to render, either within your<script>
block of a.vue
single file component (no<template>
tag needed then) or from a pure.js
file. - Into
render
, you'll pass (and use) thecreateElement
function (often shortened toh
) to create eachVNode
(virtual nodes) that Vue then handles. - Everything you normally do within the
template
tag is basically sugar coating for the actually usedrender
function.
Simple example:
render(createElement) {
return createElement(
'h1', // the element you want to render, could also be a Vue component
{
// this is the options object which is… hehe… optional, e.g.:
class: 'ohai-css',
attrs: {
id: 'mightyID'
},
on: {
click: this.handleClick
},
},
'Hello world!' // the content – a text string or an array of other VNodes
)
}
So let's have a look at how I fought my way towards a working tabs component. We'll take my AHA moments as guideposts.
this.$slots.default
is always filled!
Something I had never thought about (but makes sense): Even if you have a "closed" component, you can throw any content into it and it is available under this.$slots.default
. Check the HelloWorld.vue
in this code sandbox. The content is not rendered in the component, but it is there.
With that, you can easily filter the components as needed – in my case, it was enough to check for the name of the component:
const tabItems = this.$slots.default
.filter(item => item.componentOptions.tag === "MyTab")
Don't manipulate, duplicate!
So I had access this list of components within my Tabs
. My first though was: Nice, I'll just™ split this up into the tab navigation and the tab content, slap an index
plus an onClick
handler onto the tab navigation items and off we go.
That totally did NOT work 😄
What I had to do instead was to take the list of navigation items, create a new element for each one and add the necessary props to that component instance:
const tabItems = this.$slots.default
.filter(item => item.componentOptions.tag === "MyTab") // filter for navigation items
.map((item, index) =>
h( // render a new component…
MyTab, // of the same type
{
props: { // pass props
index,
isActive: this.selectedIndex === index // selectedIndex is declared within data
},
on: {
onClick: this.switchTab // which takes the emitted index and sets selectedIndex to that
}
},
item.componentOptions.children // use content from the original component
)
);
My uneducated, clueless guess here is: The components are already rendered. Vue doesn't let you touch them or alter their props within the render
function because that will break… the internet? 😄
You have to render completely new component instances instead. This most certainly makes sense – if you know why, please explain in the comments 😉
Reading documentation carefully actually helps!
Having achieved all of this, I was very happy and wanted to render the tab navigation and the current content like this:
return h(
"div", // render a surrounding container
[ // with children
h("ul", { class: "tabListNav" }, tabItems), // tab navigation
h('main', tabContent) // current tab content
])
Aaaand… no content was rendered ¯\_(ツ)_/¯
So I re-read the createElement
arguments part of the documentation again. And of course, it was a very simple fix: You can either pass a string as the child of an element. Or an array of items. Even if you just want to render one item, you have to put it into an array. Spot the difference:
return h(
"div", // render a surrounding container
[ // with children
h("ul", { class: "tabListNav" }, tabItems), // tab navigation
h('main', [tabContent]) // current tab content passed in an array
])
🎉 With all of this, we have a nice tab component that fulfills everything I needed:
- Render a tab navigation
- Render the correct content
- Easy to use because state handling etc. is tugged away in
<AwesomeTabs>
Of course, you could add a great deal of functionality, but I don't need to 😄
Here's a code sandbox with everything in it:
Top comments (2)
Thank you for the enlightening post :)
Tiago, thank you for reading :) Hope it was helpful!