Creating Multi-root Vue.js Components
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:
Try to build a component with a template like this:
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.
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.
SecondCell are standard Vue single file components, each with a
There are two key differences between functional components and traditional components.
- Functional components are stateless (They contain no
dataof their own, and thus their outputs are solely defined by props passed in.
- Functional components are instanceless, meaning there is no
thiscontext, instead props and related values are passed in via a
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:
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!