A common constraint in component-based frameworks like Vue.js is that each component has to have a single root element. This means that everything in a particular component has to descend from a single element, like this:
<template>
<div> <!-- The root -->
<span></span> <!-- now we can have siblings -->
<span></span>
</div>
</template>
Try to build a component with a template like this:
<template>
<span></span> <!-- two siblings at the top level of the hierarchy! -->
<span></span>
</template>
and you will get the dreaded error: Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
In the vast majority of situations this constraint causes no problems. Have 2 elements that have to go together? Simple add another layer in the DOM hierarchy and wrap them in a div. No problem.
However, there are certain situations in which you cannot simply add an additional layer of hierarchy, situations where the structure of the DOM is super important. For example - I recently had a project where I had two <td>
elements that always had to go right next to each other. Include one and you had to include the other. Logically, they were a single component, but I couldn't just wrap them in a wrapper because <td>
elements need to be direct descendants of a <tr>
to work properly.
The Solution: Functional Components
The solution to this problem lies in an implementation detail of Vue.js. The key reason why Vue cannot currently support multi-root components lies in the template rendering mechanism - Templates for a component are parsed into an abstract syntax tree (AST), and an AST needs a root!
If you sidestep template rendering, you can sidestep the single-root limitation.
Its less commonly used, but it is entirely possible to implement a Vue.js componenent without a template at all, simply by defining a render
function. These components, known as functional components, can be used for a myriad of purposes, including rendering multiple roots in a single component.
The Code
For simplicity, I wrote each of my paired <td>
elements as its own single-file component, and then simply wrapped them in a functional component that passed along props to both of them.
/* paired-cell.js */
import FirstCell from '~/components/paired-cell/first-cell';
import SecondCell from '~/components/paired-cell/second-cell';
export default {
functional: true,
props: ['person', 'place', 'thing'],
render(createElement, context) {
const first = createElement(FirstCell, { props: context.props });
const second = createElement(SecondCell, { props: context.props });
return [first, second];
},
};
FirstCell
and SecondCell
are standard Vue single file components, each with a <td>
element as the root. But PairedCell is different - it is a pure JavaScript file that exports a functional component.
There are two key differences between functional components and traditional components.
- Functional components are stateless (They contain no
data
of their own, and thus their outputs are solely defined by props passed in. - Functional components are instanceless, meaning there is no
this
context, instead props and related values are passed in via acontext
object.
Looking at what the code is doing then, it states that the component is functional, declares a set of accepted props (a person, place, and a thing), and defines a render
function that takes two arguments: createElement
and context
.
Those two arguments will be provided by Vue. createElement
is a function that sets up an element in Vue's virtual DOM. You can directly pass it element properties, but in this case I'm simply using it to render the subcomponents.
The second argument contains the context for the component; in this example the only thing we care about is the props
which we're passing along, but it also contains things like children, slots, parent, and more - all the things you might need to implement a component.
So to break down what we're doing - we implement a component that accepts a set of props, renders out two child components as siblings, and returns them as an array. Woot! A multi-root component!
P.S. — If you’re interested in these types of topics, I send out a weekly newsletter called the ‘Friday Frontend’. Every Friday I send out 15 links to the best articles, tutorials, and announcements in CSS/SCSS, JavaScript, and assorted other awesome Front-end News. Sign up here: https://zendev.com/friday-frontend.html
Top comments (2)
`
Hi Kevin ! Could you - like I'm five - explain the use of tilde at the from path when you import your resources ?
Sure. I realized after digging into this, this may actually be a Nuxt-specific thing (I use Nuxt.js for pretty much all my projects) rather than Vue generally, but in Nuxt, '~' is an alias for the root of your project.
Not sure on that, though '~' is often an alias for a similar thing. On linux or mac systems, in the shell '~' refers to your home directory.
So in this context, I'm using the tilde to represent "start from the root of the project, then navigate this directory tree". I prefer this to relative paths for importing, because it means if I move the component (or copy the import code for somewhere else) it will still work.