tsctao
tsctao

Reputation: 108

How can I wrap each element in a slot?

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

Answers (3)

Roxy Walsh
Roxy Walsh

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

user3603692
user3603692

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

Emīls Gulbis
Emīls Gulbis

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

Related Questions