Closed tochoromero closed 3 years ago
Can you build a specific scenario in which you need a template with multiple root nodes? I've been using Bootstrap and tables with Vue since the beginning and have never needed multiple root nodes.
Provide a custom attribute to indicate to what element they should be attached to.
FYI you can use $listeners
and $props
to easily pass down things to the element you want
You can have multiple root elements in functional components:
render: h => [h('p', 'one'), h('p', 'two')]
Personally, I actually like the fact that we only have one root as it clears out the questions you just listed because there's only one possible answer
On my very specific Table use case I have a Category
component that I would like to contain two <tbody>
root elements. In the first <tbody>
I will show the Category information, in the second <tbody>
I will list all the Category Items with its details. And the main Category body will control the visibility of the second one. I want it to look something like this:
Category.vue
<template>
<tbody>
<tr @click="showItems = true">
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
</tr>
</tbody>
<tbody v-show="showItems">
<tr v-for="item in category.items">...<tr>
</tbody>
</template>
Now I know I could move them to two separate components and have them communicate, but they really belong together, they have computed properties and methods they will share, and yes I can move those to a mixin, but that just makes everything a bit more complex.
Now, regarding Bootstrap, I recently stumbled on this:
I was using an input-group
and I needed to have something like this:
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-dollar"></i></span>
<input v-show="isDisabled" class="form-control form-control-sm"/>
<vue-numeric v-show="!isDisabled" class="form-control"/>
</div>
Now, the input/vue-numeric pair is reusable, I actually have quite some logic around those two, so I wanted to reuse them, but the input-group itself is not reusable, since in some places I don't need an input group or the input group is very different.
Now when I move the <input>
and the <vue-numeric>
into its own component I'm forced to use v-if
and v-else
. Unfortunately, that caused a whole lot of other problems, I needed both elements to be mounted at all times, and working around it wasn't fun, my problems would be gone if I could just have them both mounted and just v-show
them as necessary. Of course, I found a way to work around it, but it wasn't the clean simpler approach I wanted and that I can achieve in other frameworks.
I truly love Vue, this is really just neat picking. It would just be nice to have the option. The nice thing about this feature is that if you don't want to you don't have to use it even think about it, just keep wrapping everything in a div.
FYI you can use $listeners and $props to easily pass down things to the element you want
I was wondering about that, I remember reading that long time ago, but couldn't find it, thanks for the tip
The new react version has Fragment: https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html. They can do:
<template>
<> <!-- render nothing -->
<tbody>
<tr @click="showItems = true">
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
</tr>
</tbody>
<tbody v-show="showItems">
<tr v-for="item in category.items">...<tr>
</tbody>
</>
</template>
What about using this syntax in Vue ?
Honest question here, why do we even need to wrap everything on a single root element, is this a technical limitation or an arbitrary decision?
Technical, due to how the diff algorithm is written. It's obviously possible to update it, but it takes significant changes to the current algorithm (React did that during a complete rewrite).
Got it, thank you for the details
@sirlancelot One case is when you're using Nuxt, where everything is a Vue SFC.
Honest question here, why do we even need to wrap everything on a single root element, is this a technical limitation or an arbitrary decision?
Technical, due to how the diff algorithm is written.
How's this very technical to implement?
<template>
<tbody></tbody>
<tbody></tbody>
</template>
can easily be transpiled to
render: h => [ h('tbody'), h('tbody') ]
thus solving the problem (idk what it was doing before, but this is easy to do).
Here's proof that it works: https://jsfiddle.net/b049qboe/1
It doesn't seem like that requires any update to the diff algo.
@trusktr The technical challenge is not with the conversion of template to render function, it's with the implementation of the virtualdom which the render function builds nodes for.
Each child component is represented in its parent virtual dom by a single vnode
. In the current implementation, the diffing algorithm (responsible for comparing the current with the old virtualDOM and patching differences into the real DOM) can rely on the fact that every vnode of a child component has a single matching HTML element in the real dom, so the next vnode in the virutalDOM after the child component vnode is guaranteed to match the next HTML Element in the real DOM.
(Sidenote about your fiddle: functional components don't have that restriction because the are not represented with a vnode in the parent, since they don't have an instance and don't manage their own virtualdom)
Allowing fragments requires significant changes to that algorithm, since we now would somehow have to keep the parent informed at all times about how many root nodes the child is currently managing in the real DOM, so when the parent re-renders, it knows how many HTML-Elements it has to "skip" to reach the next HTML Element that doesn't belong to the child component,
That's a very intricate/complicated piece of code at the heart of Vue, and it is critical for render performance - so it's not only important to make it work correctly but also to make it highly performant
That's a pretty hefty task. As Evan mentioned, React waited for a complete re-write of its rendering layer to remove that restriction.
Are you implying that opting not to convert multiple roots from a template into multiple roots in a render function (which works) is because of performance, but it nonetheless would work? Can you make a fiddle that shows when it doesn't work?
Are you implying that opting not to convert multiple roots from a template into multiple roots in a render function (which works) is because of performance, but it nonetheless would work?
No. I'm saying that the current virtualDOM diff&patch algorithm heavily relies on the fact that each child component always has exactly one root element, so it would break completely with more than one root node in a child component.
And I'm saying that making it work with more than one root component is more complicated, it adds additional logic, so it's a challenge to make this change without negatively impacting render performance in the current implementation.
I'm still not convinced that multiple roots is even needed. If the core team is currently working on supporting it then that's great, but I don't feel like they should if it's not on the roadmap already.
Every time I've thought "Hey I might need multiple root nodes for this," it puts me on a dangerous path of adding too much complexity to a single component. I always end up with a better, simpler solution that lives well within the single root node paradigm.
Most of my solutions for the above usually rely on scoped slots. I highly recommend learning how to use them well. You will never need to think about multiple root nodes again.
If the core team is currently working on supporting it then that's great, but I don't feel like they should if it's not on the roadmap already.
It's a "nice to have" on the roadmap, and won't happen anytime soon, as it would also be a breaking change - any 3rd-party component written with multiple root nodes wouldn't work with 2.x
.
Otherwise, solid advice about complexity and scoped slots.
Can one of you show how to rewrite my above fiddle with "scoped slots" to show that it is possible to output two <tr>
elements at a time from a component, into a <table>
of an outer component (just like my example), using template(s) instead of render function(s).
When I tried using slots, it would'nt let me put a slot element as root of a component.
@LinusBorg, will the ability to return an array from a render
function be removed?
@LinusBorg, will the ability to return an array from a render function be removed?
No, why would you think we would remove anything?
Returning an array from a render function doesn't work, never worked and will continue not to work in Vue 2 - the notable exception were, are and will be functional components, for the reasons l laid out further up.
If you need help with a specific challenge in implementing a feature, forum.vuejs.org or chat.vuejs.org are the approriate place, not this issue.
functional components don't have that restriction because the are not represented with a vnode in the parent, since they don't have an instance and don't manage their own virtualdom
Last I checked (though possibly this has changed since then), a Vue SFC with <template functional>
did not support multiple "root" elements within the <template>
. Given the above comment, would it be fairly straightforward to allow that within functional template SFCs? I think that might go a long way to addressing many of the desired use-cases for it without requiring a rewrite of any fundamental architecture?
Oh, I see, it only works with functional:true
.
Well, I don't need help writing single-root components or converting into single-root components.
It's just that having multiple roots allows for useful patterns where otherwise certain patterns are not doable without multi-root components. This is why React added the <>
because community needed it.
So, I was trying to understand why it was removed when it is useful.
So, this issue is just hoping that the feature is added because it seems valid to let people compose more freely.
FWIW, this sort of composing isn't currently doable with Custom Elements. Only virtual component systems allow this at the moment, as the component instances don't actually get placed into the DOM.
A display: contents
CSS style is coming soon (currently behind flags), which will allow Web Components to achieve the same thing (i.e. to achieve the same as what multi-root rendering would in Vue among other things)
For example, here's some chatter about the same problems on w3c/webcomponents and StackOverflow.
So, I was trying to understand why it was removed when it is useful.
If that refers to the "removal" between Vue 1 and Vue 2: It was a design decision mainly because it's much easier, performant and was how virtualdoms generally worked (Vue 1 didn't use a virtualdom).
Again, React had to rewrite the whole rendering engine to get a setup that does it in a performant way.
We understand its useful in some scenarios, but there's a tradeoff between usefulness and the amount of work required to make it happen.
We understand its useful in some scenarios, but there's a tradeoff between usefulness and the amount of work required to make it happen.
@LinusBorg: any thoughts on my earlier question about allowing it in functional template SFCs? Would that be sufficiently low work?
TBH I wasn't aware that this doesn't work, still have to test this.
But yeah, rust should be a fixable problem, it's "only" about template compilation.
It would be nice if <template functional>
allowed multiple root elements.
Could someone open an issue in vue-loader, then?
Having multiple root elements in template would be nice and for some js libraries or ui frameworks it seems necessary. So far i've reached to a workaround like this: Defining a dummy component:
<template>
<div>
<!-- dummy -->
<slot></slot>
</div>
</template>
<script>
export default {
data: function() {
return {
parent: null,
children: null
}
},
beforeMount: function() {
this.parent = this.$el.parentElement
this.children = this.$el.childNodes
this.parent.removeChild(this.$el)
jQuery(this.parent).append(jQuery(this.children))
},
mounted: function() {
console.log('Dummy:', this.children)
}
}
</script>
Then use it like:
<template>
<Dummy>
<Header />
<Nav />
<section class="container">
<div>test</div>
</section>
</Dummy>
</template>
<script>
import Header from '~/components/Header'
import Nav from '~/components/Nav'
import Dummy from '~/components/Dummy'
export default {
components: {
Dummy,
Header,
Nav
},
mounted: function() {
console.log("index was mounted", this);
}
};
</script>
By the way i couldn't manage to fix it without using jquery ;)
Here is another use case I'm struggling with:
My parent component defines a grid with multiple named template-areas. Ideally my child component would do:
<template>
<div class="gridareaone">Stuff</div>
<div class="gridareatwo">Other stuff</div>
</template>
But since the child component has to have a single wrapping element, it doesn't work. I can work around it, but it causes problems.
Could someone open an issue in vue-loader, then?
I'm also struggling with the lack of support for multiple root elements in components. I have reviewed the comments in this issue and written up my problem below.
My layout uses CSS grid. In a CSS grid, the grid areas apply to the direct child of the display: grid
element. Since a Vue component must have a single root element, it is not possible to have a component represent multiple grid areas (e.g. multiple rows or columns). This is essentially the same as the problem @tochoromero raised where a single component cannot represent multiple <tbody>
elements. For my use case, the best workaround I have found is to forgo a component and use <template>
instead (second example below).
@trusktr mentioned that display: contents
is coming soon. display: contents
can be used as an imperfect workaround for this problem for CSS grids. I say imperfect because display: contents
is not semantically equivalent to replacing the parent div with its children in the DOM. This can be seen in the first example below where the#app > div
selector applies to the display: contents
div and not its children.
In response to @sirlancelot I have created some fiddles which show the issue. I am keen to see how @sirlancelot would work around this issue (and the <tbody>
problem), since it's useful to have a range of options available.
Here's an example using display: contents
and a single root div in the component:
https://jsfiddle.net/avv3vgae/28/
Here's an example of the <template>
workaround:
https://jsfiddle.net/h7rav21u/13/
With tables, if you want a component to represent multiple <tr>
that's possible by using <tbody>
as the root element. As discussed, it is not possible for a component to represent multiple <tbody>
and I don't think display: contents
can help us here.
@LinusBorg if you'll permit me a meta question: where is the best place to continue discussion of the implementation of this feature?
@euoia There's not a real place for that yet, but we are in the middle of setting up a seperate repository where RFCs can be suggested and discussed with the community. That would probably be the best place for this.
It will be available soon and will be properly announced when the time has come.
I have a pretty compelling real-world case where I'm not sure how I can proceed without having access to multiple root nodes.
I'm developing what's essentially a table+accordion. That is, the table adds more rows when you click on it to show more details for that entry.
My table is located in the Researcher Groups component, which has a sub-component: "app-researcher-group-entry (ResearcherGroupEntry).
I'm using Vue-Material, but the problem would be the same if I was using a plain
Anyway, my structure is kind of like:
// ResearcherGroups.vue
<template>
<md-table>
<md-table-row>
<md-table-head></md-table-head>
<md-table-head>Researcher</md-table-head>
<md-table-head>Type</md-table-head>
<md-table-head></md-table-head>
<md-table-head></md-table-head>
</md-table-row>
<app-researcher-group-entry v-for="(group, index) in researcherGroups" :key="group.label" :groupData="group" :indexValue="index"></app-researcher-group-entry>
</md-table>
</template>
And in ResearcherGroupEntry.vue:
<template>
<md-table-row>
<md-table-cell>
<md-button class="md-icon-button md-primary" @click="toggleExpansion">
<md-icon v-if="expanded">keyboard_arrow_down</md-icon>
<md-icon v-else>keyboard_arrow_right</md-icon>
</md-button>
<md-button @click="toggleExpansion" class="index-icon md-icon-button md-raised md-primary">{{indexValue + 1}}</md-button>
</md-table-cell>
<md-table-cell>{{groupData.label}}</md-table-cell>
<md-table-cell>Group</md-table-cell>
<md-table-cell>
<md-button @click="openTab" class="md-primary">
<md-icon>add_box</md-icon>
Add Client / Client Type
</md-button>
</md-table-cell>
<md-table-cell>
<md-button class="md-primary">
<md-icon>settings</md-icon>
Settings
</md-button>
</md-table-cell>
<app-add-client-to-group-tab :indexValue="indexValue" :groupData="groupData" :closeTab="closeTab" :addClientToGroupTab="addClientToGroupTab"></app-add-client-to-group-tab>
</md-table-row>
</template>
Here's where the problem comes in. I want to be able to expand the clients like this from our mockup:
This would be trivially easy if I could just add another
If I wasnt using Vue Material, I might have considered using JSX for this component, because then I could simply use .map in the parent to create an array of components based on the two variables. I might still be able to do that with v-for directives, but I'm not sure. What I may have to do is create, from props, a crazy pseudo array merging in parents and children, but that's UUUUUGLY code.
I feel like a compelling reason is simply that we shouldn't be forced into using arbitrary <div />
's in SFC's, especially when there are many use cases that these <div />
s cause problems. If my component or page's root element is <ul />
, but I want to overlay a <Loader />
on the list while it loads, I now have to wrap my <ul />
and <Loader />
into a <div />
that I do not need.
Whether the solution is passing functional
and adding a <Fragment />
, I don't think it matters, this needs a solution.
It's a real problem that people are using hacks to solve, like the above jQuery
frightening and clever piece above.
I think we all agree to say that is a missing feature. The Vue developpers say that is currently difficult to implement because of the diff algorithm. One of the solution for now is to not create a child component when we have this use case. We can split the parent component into multiple render functions and compose our parent. (It is easier with JSX)
For this, we can do:
const genChildren = h => {
return [h('div', "someStuff"), h(div, "Other stuff")];
}
// parent component
render(h) {
return h('myGridWrapper', genChildren(h))
}
@titouancreach How would this work? you're not getting the multiple root elements error? I assume myGridWrapper
would be a render-less component (rendering a slot with a render function). Then, whatever you throw in it, you're going to get the error. I tried pretty much everything, but to no-avail.
@adi518
That's not a workaround... It's just adding a wrapping <myGridWrapper>
(basically the same as a div
) around multiple nodes.
The only workaround is to use a functional component that doesn't have this single root limit, but you lose data/methods that normal components have.
I'm saying that the current virtualDOM diff&patch algorithm heavily relies on the fact that each child component always has exactly one root element, so it would break completely with more than one root node in a child component.
And I'm saying that making it work with more than one root component is more complicated, it adds additional logic, so it's a challenge to make this change without negatively impacting render performance in the current implementation.
Just as an FYI, Preact is currently trying to figure this out as well. https://github.com/developit/preact/issues/946 is the issue asking for it, and https://github.com/developit/preact/pull/1080/files is a current pull request for it. Just linking that in case it gives any Vue rendering engine developers/maintainers any inspiration or useful ideas. Obviously, they're different engines, so apologies if there's nothing helpful there.
Here's another use case. If you know how I could do this without multiple root nodes, please do tell, I'm kindof stumped.
Some details:
<template lang="html">
<!-- 2D Target box to intercept mouseclicks on the 3D targets -->
<div id="targetBox" @click="goto" @mouseenter="hover" @mouseleave="unhover"></div>
<!-- Details Popup -->
<div v-show="tShow" id="contextPopup">
<h3 class="name">{{ tName }}</h3>
<hr>
<ul class="functions">
<li v-for="func of tFunctions">{{ func }}</li>
</ul>
<div :class="[ 'triangle', 'small', tCorner ]"></div>
<div :class="[ 'pointer', tCorner ]"></div>
</div>
</template>
<script>
export default {
name: 'Popups',
data() {
return {
tShow: false,
tName: '',
tFunctions: [],
tCorner: ''
}
},
methods: {
goto() {
let target = document.getElementById('targetBox').dataset.dest
if (target == this.$route.name) return
this.$router.push({ name: target })
this.tShow = false
},
hover() {
let target = document.getElementById('targetBox')
let popup = document.getElementById('contextPopup')
// Update context popup info
this.tName = target.dataset.name
this.tFunctions = target.dataset.functions.split(';')
// Figure out which corner to render contextPopup
let l = (target.dataset.l == 'true') ? 'l' : 'r'
let t = (target.dataset.t == 'true') ? 't' : 'b'
this.tCorner = t + l
// Position in a corner
popup.style.left = target.dataset.popLeft + 'px'
popup.style.top = target.dataset.popTop + 'px'
// Render the popup
this.tShow = true
},
unhover() { this.tShow = false }
},
}
</script>
@titouancreach That will work only with a functional component. There's no way out of it, aside for jQuery hacks.
@adi518 This will work with anything because we don't create any children here, we are just splitting (compose) the render function. It's just pure javascript.
@tochoromero how did you solve your input-group-addon
issue with Bootstrap? I came here via the same problem, that Bootstrap has really specific ordering for these addons and an extra wrapping div breaks it.
Update: there's a big discussion about this issue on Bootstrap, including some mentions of Vue - if anyone else runs into this, there are many workarounds there. https://github.com/twbs/bootstrap/issues/23454
@bbugh I really didn't solve it, I just tried to make my component as generic as possible but still wrapping it on a div
with the input-group
class :(
Another possible workaround, besides using display: contents
, is using https://linusborg.github.io/portal-vue. With portal-vue
, the grid could be set up something like this:
<!-- grid-container.vue -->
<portal-target multiple name="grid" />
<!-- grid-child-1.vue -->
<portal to="grid">
<div>child 1 content</div>
<div>more child 1 content</div>
</portal>
<!-- grid-child-2.vue -->
<portal to="grid"><div>child 2 content</div></portal>
The above will render the following:
<div class="vue-portal-target">
<div>child 1 content</div>
<div>more child 1 content</div>
<div>child 2 content</div>
</div>
@chipit24 Yes, but that solution still ends with a parent element. The whole point is being able to render without a parent, aka Fragment.
@adi518 but Fragment
is a parent element but is a virtual one so I am confused with what you mean by that.
Should it render any element? no it shouldn't Could be like Fragment? absolutely, the whole issue with this is rendering an extra element, no necessarily using another component as a wrapper.
<template>
<vue-fragment>
.... render as many component you want, fragment will no create any wrapper component.
</vue-fragment>
</template>
@adi518 The portal
elements in my example do not render a parent element, so I'm not sure what you mean.
This is a top hit for me in Google, and I'm posting an example that wasn't mentioned before of how to work around not having actual fragments. The example I posted works for my use case.
It won't work in the sense that the Portal component adds its wrapper element, as it is a stateful component <div class="vue-portal-target">{{ $children }}</div>
. You are still constrained. Your solution is good for the case you used, definitely. When you really need no parent elements at all you are stuck until Vue actually adds this functionality. I'm checking out the display: contents
hack. Maybe that will suffice for the time being.
I'm sorry if i'm posting stupid answer. I'm still rookie in VueJS. I think I kinda found a suitable solution which supports all the requirements : fragment can be root, and are not functional.
Can you guys check this : https://www.npmjs.com/package/vue-fragments ? here's my experimental jsfiddle
@adi518 my understanding of vDOM doesn't allow me to do it for SSR. My main problem is about finding the parent of that fragment enabled node. it seems impossible to find the exact place it is inserted and i don't know why. such a mystery is ... mesmerizing. If anybody can help, that would be more than welcome...
@y-nk Done some adjustments to your solution: https://github.com/y-nk/vue-fragments/issues/1
This allowed me to fix my Breakpoint component. 👍
I can do this now (nothing rendered other than those elements):
<v-show-at small>
<span>It</span>
<span>Actually</span>
<span>Works</span>
</v-show-at>
Will have to add SSR to known issues, unless we can fix that one as well.
@adi518 thanks a lot for your feedbacks/corrections :) i'm trying hard to work on SSR but it's a case i cannot really crack yet.
What problem does this feature solve?
Right now you can only have 1 root element per template. I know this is by design, but I find myself wrapping everything around a
<div>
a lot. Now, most of the time is not a big deal, I can live with that, the problem is when either Bootstrap requires a very specific hierarchy, or when dealing with Tables that also require a very specific hierarchy and wrapping everything on a div is not an option.What does the proposed API look like?
Now, there will be a couple of things to figure out, mainly to what element will the properties provided on the Custom Element will be attached to. I think there two things that can be done: 1) Find the first child and stick everything to it. 2) Provide a custom attribute to indicate to what element they should be attached to.