Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow more than 1 root element for Template #7088

Closed
tochoromero opened this issue Nov 20, 2017 · 56 comments
Closed

Allow more than 1 root element for Template #7088

tochoromero opened this issue Nov 20, 2017 · 56 comments

Comments

@tochoromero
Copy link

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.
@sirlancelot
Copy link

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.

@posva
Copy link
Member

posva commented Nov 20, 2017

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

@tochoromero
Copy link
Author

tochoromero commented Nov 20, 2017

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.

@tochoromero
Copy link
Author

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

@titouancreach
Copy link

titouancreach commented Nov 30, 2017

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 ?

@tochoromero
Copy link
Author

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?

@yyx990803
Copy link
Member

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).

@tochoromero
Copy link
Author

Got it, thank you for the details

@pxwee5
Copy link

pxwee5 commented Dec 1, 2017

@sirlancelot One case is when you're using Nuxt, where everything is a Vue SFC.

@trusktr
Copy link

trusktr commented Jan 16, 2018

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).

@trusktr
Copy link

trusktr commented Jan 16, 2018

Here's proof that it works: https://jsfiddle.net/b049qboe/1

It doesn't seem like that requires any update to the diff algo.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 16, 2018

@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.

@trusktr
Copy link

trusktr commented Jan 16, 2018

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?

@LinusBorg
Copy link
Member

LinusBorg commented Jan 16, 2018

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.

@sirlancelot
Copy link

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.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 16, 2018

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.

@trusktr
Copy link

trusktr commented Jan 16, 2018

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
Copy link
Member

LinusBorg commented Jan 16, 2018

@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.

@effulgentsia

This comment has been minimized.

@trusktr

This comment has been minimized.

@LinusBorg

This comment has been minimized.

@effulgentsia

This comment has been minimized.

@LinusBorg

This comment has been minimized.

@danielsvane

This comment has been minimized.

@LinusBorg

This comment has been minimized.

@mese79

This comment has been minimized.

@chipit24

This comment has been minimized.

@adi518

This comment has been minimized.

@yordis
Copy link

yordis commented Jun 11, 2018

@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>

@chipit24

This comment has been minimized.

@adi518
Copy link

adi518 commented Jun 11, 2018

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.

@y-nk
Copy link

y-nk commented Jun 13, 2018

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

@y-nk

This comment has been minimized.

@adi518

This comment has been minimized.

@y-nk

This comment has been minimized.

@y-nk

This comment has been minimized.

@rentalhost

This comment has been minimized.

@effulgentsia

This comment has been minimized.

@rentalhost

This comment has been minimized.

@sirlancelot
Copy link

Two solutions have existed for quite some time now:

@posva Can we get this thread locked so that future readers will see these solutions?

@vuejs vuejs deleted a comment from titouancreach Sep 15, 2018
@vuejs vuejs deleted a comment from kerryboyko Sep 15, 2018
@vuejs vuejs deleted a comment from adi518 Sep 15, 2018
@vuejs vuejs deleted a comment from titouancreach Sep 15, 2018
@vuejs vuejs deleted a comment from adi518 Sep 15, 2018
@vuejs vuejs deleted a comment from titouancreach Sep 15, 2018
@vuejs vuejs deleted a comment from erikweebly Sep 15, 2018
@vuejs vuejs deleted a comment from adi518 Sep 15, 2018
@vuejs vuejs deleted a comment from JoshuaSoileau Sep 15, 2018
@vuejs vuejs deleted a comment from adamchenwei Sep 15, 2018
@vuejs vuejs deleted a comment from lebesnec Sep 15, 2018
@vuejs vuejs locked as off-topic and limited conversation to collaborators Sep 15, 2018
@posva
Copy link
Member

posva commented Feb 24, 2021

Closing as this was implemented in Vue 3 but cannot be backported to Vue 2

@posva posva closed this as completed Feb 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests