How to use <slot/> as the root element of a Vue component? - vue.js

I'd like to build a wrapper component, that just adds some JavaScript to the actual component that is passed in, but without creating an extra HTML element.
This works, but creates a div as the root element:
<template>
<div v-observe-visibility="{ callback: isViewableNow, once: true }"
:class="{ 'visible animated slideInRight': showAnimation, 'invisible': !showAnimation }">
<slot/>
<div/>
</template>
I'd like something similar, without adding the div, like:
<template>
<slot v-observe-visibility="{ callback: isViewableNow, once: true }"
:class="{ 'visible animated slideInRight': showAnimation, 'invisible': !showAnimation }"/>
</template>
This, however, gives me the error:
Cannot use as component root element because it may contain multiple nodes.
Is this possible in any other way in Vue?

Related

My dynamic component (layout) doesn't work with named slots in vuejs

I have problems to combine dynamic generated layouts with named slots.
To define my layouts I'm using "component :is"
//app.vue
<template>
<component :is="layout">
<router-view />
</component>
</template>
<script>
computed: {
layout() {
const layout = this.$route.meta.layout || 'default'
return () => import(`#/app/layouts/${layout}.vue`)
}
},
</script>
//layouts/default.vue
<template>
<div>
<div>
<slot name="header" />
</div>
<div>
<div>
<slot name="sidebar" />
</div>
<div>
<slot name="default"/>
</div>
</div>
</div>
</template>
// views/page.vue
<template>
<div>
<template #header>
<h1>Primitives</h1>
</template>
<template #sidebar>
<ul>
<li v-for="primitive in sections.sections" :key="primitive">
<router-link :to="`/primitives/${primitive}`">{{primitive}}</router-link>
</li>
</ul>
</template>
<template #default>
<router-view :key="$router.path" />
</template>
</div>
</template>
But now I get this error inside my code
'v-slot' directive must be owned by a custom element, but 'div' is not.
and console displays this error
<\template v-slot> can only appear at the root level inside the receiving component
If I remove the main div I get the error
The template root requires exactly one element.
What I'm doing wrong?
This is not easy to explain so please cope with me...
I really understand what you are trying to do but unfortunately it is not possible in Vue.
Reason for that is slots are more template compiler feature than runtime feature of Vue. What I mean by that ? When Vue template compiler sees something like <template #header>, it will take the inner content and compile it into a function returning virtual DOM elements. This function must be passed to some component which can call it and include the result in it's own virtual DOM it is generating. To do that template compiler needs to know to what component it should pass the function (that is the real meaning of 'v-slot' directive must be owned by a custom element, but 'div' is not. error message...ie compiler is "looking" for a component to pass the slot content to...)
But you are trying to use the slots as if they were "discoverable" at runtime. For your code to work the dynamic layout component must at runtime somehow discover that it's child (also dynamic thanks to <router-view />) has some slot content it can use. And this is not how slots work in Vue. You can pass the slot content your component receives from parent to a child components but do not expect that parent component (layout in this case) can "discover" slot content defined in it's child components...
Unfortunately only solution for your problem is to import the layout component in every "page" and use it as a root element in the template. You can use mixins to reduce code duplication (to define layout computed)
#/mixins/withLayout.js
export default = {
computed: {
layout() {
const layout = this.$route.meta.layout || 'default'
return () => import(`#/app/layouts/${layout}.vue`)
}
}
}
views/page.vue
<template>
<component :is="layout">
<template #header>
<h1>Primitives</h1>
</template>
<template #sidebar>
<ul>
<li v-for="primitive in sections.sections" :key="primitive">
<router-link :to="`/primitives/${primitive}`">{{primitive}}</router-link>
</li>
</ul>
</template>
<template #default>
<router-view :key="$router.path" />
</template>
</component>
</template>
<script>
import withLayout from '#/mixins/withLayout'
export default {
mixins: [withLayout]
}
</script>

Passing slots through from Parent to Child Components

I have built a user-defined component (async-select) on top of another component (vue mutliselect) like this:
https://jsfiddle.net/2x7n4rL6/4/
Since the original vue-multiselect component offers a couple of slots, I don't want to loose the chance to use them. So my goal is to make these slots available from inside my custom component. In other words, I want to something like this:
https://jsfiddle.net/2x7n4rL6/3/
But that code oes not work.
However, if I add the slot to the child component itself, it works just fine (which you can see from the fact that options become red-colored).
https://jsfiddle.net/2x7n4rL6/1/
After surfing the web, I have come across this article, but it does not seem to work
Is there any way in VueJS to accomplish this ?
Slots can be confusing!
First, you need a template element to define the slot content:
<async-select :value="value" :options="options">
<template v-slot:option-tmpl="{ props }">
<div class="ui grid">
<div style="color: red">{{ props.option.name }}</div>
</div>
</template>
</async-select>
Then, in the parent component, you need a slot element. That slot element itself can be inside of another template element, so its contents can be put in a slot of its own parent.
<multiselect
label="name"
ref="multiselect"
v-model="localValue"
placeholder="My component"
:options="options"
:multiple="false"
:taggable="false">
<template slot="option" slot-scope="props">
<slot name="option-tmpl" :props="props"></slot>
</template>
</multiselect>
Working Fiddle: https://jsfiddle.net/thebluenile/ph0s1jda/

Pass a button with click handler via slot to recursive child component

I've got a page template with the following code part:
<nested-draggable v-bind:list="list" v-bind:selected="selected" v-bind:group="dragGroup">
<slot>
<v-icon v-on:click="$root.$emit('click', el)" small v-if="allowcreate" style="float: right">mdi-plus</v-icon>
</slot>
</nested-draggable>
the sub component ("nested-draggable.vue") for the recursion looks like this:
<template>
<ul class="tree">
<draggable
class="dragArea"
tag="li"
v-for="el in list"
v-bind:elementdata="el"
v-bind:key="el._id"
v-bind:list="list_empty"
v-bind:selected="selected"
v-bind:group="group"
v-on:add="add"
>
<span v-bind:class="{'selected' : el._id === selected._id}" v-on:click="elemClicked(el)">{{ el.title }}</span>
<slot></slot>
<!-- render children of the current iterated element -->
<nested-draggable
v-bind:list="el.children" v-bind:selected="selected" v-bind:group="group">
<!--<slot></slot>-->
</nested-draggable>
</draggable>
</ul>
</template>
so I'd like to have the click event from the button within the passed slot emited with the current iteration's var "el" when the "plus" button is clicked, but within the slot the "el" var that is used within the iteration at the nested-draggable component can not be accessed. Vue tells that there is no "el" reference when trying to emit. (Throwing this error: https://pastebin.com/8bNwMcDr)
So how can I access the recursive data within the passed slot? How do I have to define my slot when passing it?
The only solution I found is putting the button/event-link directly into the nested-draggable component (not as slot) but I think to be clean and write a nice separated component, this would not belong into the nested draggable component, but in its parent.
You don't need to pass your event from the template because you can get in your method anyways. This should help you out.

How to dynamically insert a template into another template

New to VueJS... I have a component that I want to pass other components into based on the selection made in a dropdown. I have a main template that will always be rendered on the screen, part of which has a dropdown. When I make a selection in that dropdown I want to have a div inside that main component with an ID (or some other identifying property) and push another template inside of it. I'm thinking that a slot does the opposite of what I want..
Original Template:
<div class="search-field-with-label-container">
<el-select v-model="serviceType">
<el-option
v-for="serviceType in serviceTypes"
:key="serviceType.id"
:value="serviceType"
>{{ serviceType }}</el-option>
</el-select>
<div id="thisIsWhereIWantMyOtherTemplateToRender"
</div>
Second template:
<template>
<h1>this is the other template</h1>
</template>
You can use dynamic components with the keep-alive tag.
<keep-alive>
<component v-bind:is="selectedComponent"></component>
</keep-alive>
Documentation is here.

How do I use conditional rendering on template tag?

According to the Vue documentation I should be able to add the v-if condition to the <template> tag:
<template v-if="false">
<div>Invisible text</div>
</template>
But this will not hide the element, however it does work when added to the child element:
<template>
<div v-if="false">Invisible text</div>
</template>
Any suggestions?
I'm including the template in another .vue file:
<template>
<div id="app">
<H1 class= "main-title">Title</H1>
<span class="components">
<testtemplate></testtemplate>
</span>
</div>
</template>
The template tag of a single-file component is not rendered by Vue like normal <template> tags. It is simply one of the placeholders, along with <script> and <style> that vue-loader uses to build the component. The root element of that template is what will be the root in the component.
But, even if it worked the way you want, there would be no difference between your first and second example. Using v-if on the root will prevent the entire component's template from rendering if set to false.
Had this problem with VUE3. Using SFC just nest tag template inside another tag template :
<template>
<template v-if="false">
You won't see this
</template>
</template>