Vuejs nested slots: how to pass slot to grandchild - vue.js

I use different vuetify components, for example v-menu. It has a template like this:
<v-menu>
<a slot="activator">menu</a>
<v-list>
<v-list-tile>Menu Entry 1</v-list-tile>
<v-list-tile>Menu Entry 2</v-list-tile>
</v-list>
</v-menu>
Suppose I want to add another wrapper around it. That is my special menu component that has some predefined menu options. And I want it to has an activator slot as well. And the last should be somehow assigned to the original v-menu activator slot. Is it possible?
Example:
// index.vue:
<template>
<my-special-menu>
<button>My special menu trigger</button>
</my-special-menu>
</template>
// MySpecialMenu.vue
<template>
<v-menu>
<slot slot="activator"/> <-- I don't know how to write this line
<v-list>...</v-list>
</v-menu>
</template>
<slot slot="activator"> is an incorrect equation. The goal is to pull the content from the parent (that is <button>..</button> in the example), and use it as slot="activator" in v-menu.
I can write it like this:
<v-menu>
<a slot="activator"><slot/></a>
...
</v-menu>
But this case the result template will be:
<div class="v-menu__activator">
<a>
<button>My special menu trigger</button>
</a>
</div>
That's not exactly what I want. Is it possible to get rid off <a> wrapper here?
Update:
We can use a construction like <template slot="activator"><slot name="activator"/></template> to throw some slot to a grand child. But what if we have multiple slots and we want to proxy them all? That's like inheritAttrs and v-bind="$attrs" for slots. Is it currently possible?
For example, there's <v-autocomplete> component in vuetify that has append, prepend, label, no-data, progress, item, selection etc slots. I write some wrapper component around this, it currently looks like:
<template>
<v-autocomplete ..>
<template slot="append"><slot name="append"/></template>
<template slot="prepend"><slot name="prepend"/></template>
<template slot="label"><slot name="label"/></template>
...
<template slot="item" slot-scope="props"><slot name="item" v-bind="props"/></template>
</v-autocomplete>
</template>
Is it possible to avoid all slots enumeration here?

If you put the slot attribute on a html element, that html element is passed to the child component to fill the slot with that name. If you don't want to pass along a html element, you can use slot on a template tag within your component. A template tag groups elements, but does not render to a html element, which is perfect here. You can use template tags also for other things, such as to group elements in a v-if for example, or to repeat multiple elements with a v-for.
// App.vue
<template>
<div id="app">
<test>
<template slot="activator">
Click <b>me</b>!
</template>
</test>
</div>
</template>
// Test.vue
<template>
<div class="wrapper">
<grand-child>
<template slot="activator">
<slot name="activator"></slot>
</template>
</grand-child>
This is some text
</div>
</template>
// GrandChild.vue
<template>
<div>
<a href="#" #click="toggle = !toggle">
<slot name="activator">Default</slot>
</a>
<div v-if="toggle">This appears and disappears</div>
</div>
</template>
Edit: If you want to do this for arbitrary slots, this is also possible. this.$slots contains the slots and their content, so with something like the following, you can pass the slot content to a slot with the same name:
<grand-child>
<template v-for="(_, slot) in $slots">
<template :slot="slot">
<slot :name="slot"></slot>
</template>
</template>
</grand-child>
For completeness sake, scoped slots can be accessed through $scopedSlots and be propagated like so:
<grand-child>
<template v-for="(_, slot) in $scopedSlots" v-slot:[slot]="props">
<slot :name="slot" v-bind="props" />
</template>
</grand-child>
source and comment

I had EsLint errors because of the depreciated :slot and $scopedSlots attributes.
So I combined both of #Sumurai8 answers like this and it works great:
<template v-for="(_, slot) in $slots" v-slot:[slot]>
<slot :name="slot"></slot>
</template>

If you have both named and unnamed slots with props:
Vue 3
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}" />
</template>
Typescript version
<template v-for="(_, name) in ($slots as {})" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}" />
</template>

Related

Slot on Vuetify.js custom component

I have a component which renders a standard .
I would like to use slots from my component, I would like to write something like:
<MyComponent>
<header>
Titolo
</header>
<body>
my component body
</body>
</MyComponent>
then final component should be:
<v-dialog>
<h1>
// header slot content
</h1>
// body slot content
</v-dialog>
how can I do this? This only works with <slot> but not with named slot.
To use multiple slots you can use the following syntax:
<MyComponent>
<template v-slot:header>
Titolo
</template>
<template v-slot:body>
<p>my component body</p>
</template>
</MyComponent>
So you can pass some HTML in the template blocks and it will render in the component.
MyComponent.vue has the next content:
<template>
<v-dialog>
<h1>
<slot name="header"></slot>
</h1>
<slot name="body"></slot>
</v-dialog>
</template>
You can define names for your slots in your custom component by using the name attribute available for the <slot> element, e.g. <slot name="header">. If you don't define a name for the slot, its name will just be default. See the Vue.js slots documentation here: https://v2.vuejs.org/v2/guide/components-slots.html
Also, I made a simple usage example that you can check out here: https://codesandbox.io/s/unruffled-mopsa-f47hm?file=/src/App.vue
So in your case, your custom component could look something like this:
<v-dialog>
<slot name="header" />
<slot name="body" />
</v-dialog>
And to use it in the parent component, you could have:
<MyComponent>
<template v-slot:header>
Titolo
</template>
<template v-slot:body>
<p>my component body</p>
</template>
</MyComponent>

Slot and referring template in the same component in parent

I am stuck with a problem in vue 2. Basically I have a parent and child component. Basically I want to do something like this.
Parent.vue:
<template>
<div>
<Child>
<template #MyComponent>
<slot name="MyComponent" />
</template>
</Child>
<template #MyComponent>
<MyComponent/>
</template>
</div>
</template>
Child.vue:
<template>
<slot name="myComponent"/>
</template>
Can this be done in vue? I have tried to do this. But it doesn't refer to the MyComponent
Thanks in advance.
This is not, how the structure of slots work. You are calling <template #MyComponent> outside of your <Child> and trying to do something there, that is hard to understand.
I´ll asume you try to pass a component named MyComponent inside of the slot of another component named Child. This is a small example for this case:
// Parent, where you call your Child with myComponent in the slot
<Child>
<template #mySlot>
<my-component></my-component>
</template>
</Child>
// Child
<template>
<div>
<slot name="mySlot"></slot>
</div>
</template>
// myComponent
<template>
<div>
Text from myComponent.vue
</div>
</template>

Vue.js v-slot directive to replace slot directive

I am trying to mimic the default behavior of using the slot tag in a parent component. When I use <template v-slot> The content doesn't automatically go there. What is the correct way to do this without using the slot tag?, as I read that this will be depreciated. Thank you
Actually, it's the slot attribute that is deprecated (not the built-in <slot> component).
Before v2.6, the slot attribute was used to specify the name of the slot for the given content, and slot-scope received the slot props:
<base-layout>
<template slot="header" slot-scope="{ subtitle }">
<h1>Here might be a page title</h1>
<h2>{{ subtitle }}</h2>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>
From v2.6 forward, those two attributes are combined/replaced with the v-slot directive, where the slot name is given as an argument, and any slot props are received in the binding value:
<base-layout>
<template v-slot:header="{ subtitle }">
<h1>Here might be a page title</h1>
<h2>{{ subtitle }}</h2>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

How to make a component use v-for have dynamic slots

I have a child component that uses v-for. I want to be able to have the parent pass down a slot, or something similar of how it wants each item in the v-for display. However, the problem is that the parent does not have access to each individual item in the v-for as it's rendered.
Some things i've tried is passing a slot with specific keys. e.g.
<child-comp :items="items">
<div v-text="item.text" slot="body"/>
</child-comp>
Basic code may look like this for what i'm trying (though it doesn't work)
Parent component would look something like
<template>
<child-comp :items="items>
<div v-text="item.text"
</child-comp>
</template>
items = [{ text: 'hello'}]
Child would look something like this
<template>
<div>
<span v-for="item in items">
<slot></slot>
</span>
</div>
</template>
Note this has to be dynamic because one item might do v-text, another may do something like add more html such as an image, and another may do something completely different.
I believe you're looking for scoped slots.
https://v2.vuejs.org/v2/guide/components-slots.html#Scoped-Slots
Note that the preferred syntax for using scoped slots changed in Vue 2.6.0 so the exact way you write this will depend on which version you're using.
Your child would pass the item to the slot, like this:
<template>
<div>
<span v-for="item in items">
<slot :item="item"></slot>
</span>
</div>
</template>
The parent would look like this for Vue 2.6.0+:
<template>
<child-comp :items="items">
<template v-slot:default="slotProps">
<!-- The parent can put whatever it needs here -->
{{ slotProps.item }}
</template>
</template>
</child-comp>
</template>
Any props passed to the slot by the child will be included in the slotProps. There's no need to call it slotProps and in practice it is usually destructured (see the docs for more details).
For Vue 2.5 you'd use slot-scope instead:
<template>
<child-comp :items="items">
<template slot-scope="slotProps">
<!-- The parent can put whatever it needs here -->
{{ slotProps.item }}
</template>
</template>
</child-comp>
</template>
Prior to Vue 2.5.0 slot-scope was called scope.

Vue - how to pass down slots inside wrapper component?

So I've created a simple wrapper component with template like:
<wrapper>
<b-table v-bind="$attrs" v-on="$listeners"></b-table>
</wrapper>
using $attrs and $listeners to pass down props and events.
Works fine, but how can the wrapper proxy the <b-table> named slots to the child?
Vue 3
Same as the Vue 2.6 example below except:
$listeners has been merged into $attrs so v-on="$listeners" is no longer necessary. See the migration guide.
$scopedSlots is now just $slots. See migration guide.
Vue 2.6 (v-slot syntax)
All ordinary slots will be added to scoped slots, so you only need to do this:
<wrapper>
<b-table v-bind="$attrs" v-on="$listeners">
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope"><slot :name="slot" v-bind="scope"/></template>
</b-table>
</wrapper>
Vue 2.5
See Paul's answer.
Original answer
You need to specify the slots like this:
<wrapper>
<b-table v-bind="$attrs" v-on="$listeners">
<!-- Pass on the default slot -->
<slot/>
<!-- Pass on any named slots -->
<slot name="foo" slot="foo"/>
<slot name="bar" slot="bar"/>
<!-- Pass on any scoped slots -->
<template slot="baz" slot-scope="scope"><slot name="baz" v-bind="scope"/></template>
</b-table>
</wrapper>
Render function
render(h) {
const children = Object.keys(this.$slots).map(slot => h('template', { slot }, this.$slots[slot]))
return h('wrapper', [
h('b-table', {
attrs: this.$attrs,
on: this.$listeners,
scopedSlots: this.$scopedSlots,
}, children)
])
}
You probably also want to set inheritAttrs to false on the component.
I have been automating the passing of any (and all) slots using v-for, as shown below. The nice thing with this method is that you don't need to know which slots have to be passed on, including the default slot. Any slots passed to the wrapper will be passed on.
<wrapper>
<b-table v-bind="$attrs" v-on="$listeners">
<!-- Pass on all named slots -->
<slot v-for="slot in Object.keys($slots)" :name="slot" :slot="slot"/>
<!-- Pass on all scoped slots -->
<template v-for="slot in Object.keys($scopedSlots)" :slot="slot" slot-scope="scope"><slot :name="slot" v-bind="scope"/></template>
</b-table>
</wrapper>
Here is updated syntax for vue >2.6 with scoped slots and regular slots, thanks Nikita-Polyakov, link to discussion
<!-- pass through scoped slots -->
<template v-for="(_, scopedSlotName) in $scopedSlots" v-slot:[scopedSlotName]="slotData">
<slot :name="scopedSlotName" v-bind="slotData" />
</template>
<!-- pass through normal slots -->
<template v-for="(_, slotName) in $slots" v-slot:[slotName]>
<slot :name="slotName" />
</template>
<!-- after iterating over slots and scopedSlots, you can customize them like this -->
<template v-slot:overrideExample>
<slot name="overrideExample" />
<span>This text content goes to overrideExample slot</span>
</template>
This solution for Vue 3.2 version and above
<template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>