Reputation: 108
I wrote a component (Component B) that accepts list of custom components via slot like this
// Component A
<div class="component-a">
...
<component-b>
<component-x></component-x>
<component-y></component-y>
</component-b>
...
</div>
and I want to wrap component x and y in other component, such as li tag.
// Output
...
<ul>
<li><component-x></component-x></li>
<li><component-y></component-y></li>
</ul>
...
I tried with
// Component B
<div class="component-b">
<ul>
<li v-for="item in $slots.default">
<component :is="item"></component>
</li>
</ul>
</div>
It doesn't work. The item is VNode object and it can't render with component tag. Is there any solution to solve this problem?
Edit: My wrapping component is not li tag, it's a custom component with specified props that I set it in component B. If I wrap them from component A, I need to write the custom component and its props repeatedly.
Edit2: Render function maybe solve this problem, but I'm looking for solution with html template (single file component).
Upvotes: 5
Views: 3773
Reputation: 679
I was searching for a solution to the exact same problem, and eventually came across this blog post which has solved it for me in Vue2 https://www.jankollars.com/posts/vue-2-wrapping-slot-items/
I went with the final solution in the article, which I'll add here for completeness:
I have a parent component which contains a button-list component. Button-list needs to receive a number of items via a slot, AND it needs to wrap EACH item in a separate li
tag
In the button-list component, I added a components
section which defines a VNodes component (you could build this as a separate component in its own file if you prefer)
components: {
/**
* Allows us to individually render each element passed to the default slot
* Useful so that we can wrap them in other elements
*/
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes
}
}
In the template for the button-list component, I then have
<template>
<ul class="button-list">
<li
v-for="(item, i) in $slots.default" :key="i"
class="button-list__item"
>
<v-nodes :vnodes="item" />
</li>
</ul>
</template>
Note I do not have a <slot>
explicitly declared in the template - I'm not sure how exactly you'd do this with named slots, but it might be possible
Then my parent component uses button-list, and passes it a number of items (in my case another component, but you could pass whatever you need here)
<button-list class="m-button-list--padded">
<menu-button
v-for="button in buttons"
:href="button.href"
:label="button.label"
/>
</button-list>
What is output is
<template>
<ul class="button-list">
<li class="button-list__item">
< A Button />
</li>
<li class="button-list__item">
< A Button />
</li>
</ul>
</template>
Which I think is pretty much exactly what the original poster was trying to achieve - hopefully this helps someone in a similar predicament!
Upvotes: 0
Reputation:
I guess it is impossible to make that without the adult version (render function).
PS: for a more detailed component without mess with render function I suggest insert another component for handle others functionalities, eg:
createElement(componentForMakeCoolThings,{props...})
PS2: you can use the solution below in a single file component with a simple adaptation:
<script>
export default{
render: function(createElement){
}
}
</script>
Vue.component('makeLi',{
render:function(createElement){
var list = []
this.$slots.default.forEach((element,index) => {
if(element.tag){
list.push(createElement('li',{},[element]))
}
});
return createElement('ul',{},list)
}
})
new Vue({el:'#app'});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<make-li>
<h1>test1</h1>
<b>bold</b>
<i>italic</i>
</make-li>
</div>
Upvotes: 6
Reputation: 2070
This works?
<div class="component-a">
<component-b>
<component-x></component-x>
<component-y></component-y>
</component-b>
</div>
Then use
<div class="component-a">
<component-b>
<ul>
<li><component-x></component-x></li>
<li><component-y></component-y></li>
</ul>
</component-b>
</div>
Upvotes: 0