5
votes

How can I use named slots from dynamic components in the parent component?

A slider component takes an array of dynamic slide components:

<slider :slides="slides" />

Each slide has named slots with content to be using by the slider:

<template>
<div class="slide">
  <div slot="main">Slide 1 Main</div>
  <div slot="meta">Slide 1 Meta</div>
</div>
</template>

The slider should now use these slots, like so:

<template>
<div class="slider">
  <div class="slider__slide" v-for="slide in slides">
    <component :is="slide">
      <div class="slider__slide__main">
        <slot name="main" /><!-- show content from child's slot "main" -->
      </div>
      <div class="slider__slide__meta">
        <slot name="meta" /><!-- show content from child's slot "meta" -->
      </div>
    </component>
  </div>
</div>
</template>

But <component> ignores its inner content, so the slots are ignored.

Example:
https://codepen.io/anon/pen/WZjENK?editors=1010

If this isn't possible, is there another way to create a slider that takes HTML content from slide components without caring about their content?

2
Where does the content for each slide come from? - Bert
They are static (and HTML-heavy) single file components. - bernhardw
If I understand correctly, you have slide components that contain two sections that you want to arrange separately in a parent? - Bert
Exactly, the slide components provide two sections (main and meta) and the slider component knows what to do with the slides and their two sections. It needs to animate the slides and their sections separately. But the rather dumb, static slide components don't need to know of any of this. - bernhardw
If it's truly static html in the slides I would probably abandon keeping the html in components and instead use a data structure like {main: <some html>, meta: <some html>} combined with v-html. There's no simple way I can conceive of to specify a slot in which to render in a child component. Maybe someone else will have an idea how to do that. - Bert

2 Answers

2
votes

By splitting the main/meta sections into their own components, you can relatively easily use a render function to split them into the sections you want.

console.clear()

const slide1Meta = {
  template:`<div>Slide 1 Meta</div>` 
}
const slide1Main = {
  template: `<div>Slide 1 Main</div>`
}
const slide2Meta = {
  template:`<div>Slide 2 Meta</div>` 
}
const slide2Main = {
  template: `<div>Slide 2 Main</div>`
}

Vue.component('slider', {
  props: {
    slides: {
      type: Array,
      required: true
    }    
  },
  render(h){
    let children = this.slides.map(slide => {
      let main = h('div', {class: "slider__slide__main"}, [h(slide.main)])
      let meta = h('div', {class: "slider_slide_meta"}, [h(slide.meta)])
      return h('div', {class: "slider__slide"}, [main, meta])
    })
    return h('div', {class: "slider"}, children)
  }
});


new Vue({
  el: '#app',
  data: {
    slides: [
      {meta: slide1Meta, main: slide1Main}, 
      {meta: slide1Meta, main: slide2Main}
    ]
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
  <slider :slides="slides" />
</div>

<script type="text/x-template" id="slide1-template">
  <div class="slide">
    <div slot="main">Slide 1 Main</div>
    <div slot="meta">Slide 1 Meta</div>
  </div>
</script>

<script type="text/x-template" id="slide2-template">
  <div class="slide">
    <div slot="main">Slide 2 Main</div>
    <div slot="meta">Slide 2 Meta</div>
  </div>
</script>
1
votes

Actually slots within a dynamic component element do work. I have have been attempting to solve this same issue, and found this lovely little example by Patrick O'Dacre on CodePen. Patrick has made ample and useful comments in his code which I paste verbatim here for posterity. I omitted the css which you can find on CodePen.

const NoData = {
    template: `<div>
        This component ignores the data completely. 
        <p>But there are slots!</p>
        <slot></slot> 
        <slot name="namedSlot"></slot>
    </div>`
    // In this component, I just ignore the props completely
}
    
const DefaultMessage = {
    template: `<div>
        This component will show the default msg: <div>{{parentData.msg}}</div>
    </div>`,
    // this component won't have posts like the Async Component, so we just ignore it
    props: ['parentData']
}
    
const CustomMessage = {
    template: `<div>
        This component shows a custom msg: <div>{{parentData.msg}}</div>
    </div>`,
    // this component won't have posts like the Async Component, so we just ignore it
    props: ['parentData']
}
    
const Async = {
    template: `<div>
        <h2>Posts</h2>
        <p>{{parentData.msg}}</p>
        <section v-if="parentData.posts.length > 0">
            <ul>
                <li class="postInfo" v-for="post in parentData.posts">
                    <div class="postInfo__title">
                        <strong>Title:</strong> {{post.title}}
                    </div>
                </li>
            </ul>
        </section>
    </div>`,
    props: ['parentData']
}
    
/* Children should only affect parent properties via an EVENT (this.$emit) */
const ChangeMessage = {
    template: `<div>
        <p>Type here to change the message from the child component via an event.</p>
        <div><input type="text" v-model="message" @input="updateDateParentMessage" /></div>
    </div>`,
    data() {
        return {
            // initialize our message with the prop from the parent.
            message: this.parentData.msg ? this.parentData.msg : '' 
        }
    },
    props: ['parentData'],
    /* Need to watch parentData.msg if we want to continue
      to update this.message when the parent updates the msg */
    watch: {
        'parentData.msg': function (msg) {
            this.message = msg  
        }
    },
    methods: {
        updateDateParentMessage() {
            this.$emit('messageChanged', this.message)
        }
    }
};

const Home = {
    template: `<section>
        <div class="wrap">
            <div class="right">
                <p><strong>Change the current component's message from the Home (parent) component:</strong></p>
                <div><input type="text" v-model="dataForChild.msg" /></div>
                <p><strong>Important!</strong> We do not change these props from the child components. You must use events for this.</p>
            </div>
        </div>

        <div class="controls">
            <button @click="activateComponent('NoData')">No Data</button>
            <button @click="activateComponent('DefaultMessage')">DefaultMessage</button>
            <button @click="activateComponent('CustomMessage', {posts: [], msg: 'This is component two'})">CustomMessage</button>
            <button @click="getPosts">Async First</button>
            <button @click="activateComponent('ChangeMessage', {msg: 'This message will be changed'})">Change Msg from Child</button>
            <button @click="deactivateComponent">Clear</button>
        </div>

        <div class="wrap">
            <div class="right">
                <h2>Current Component - {{currentComponent ? currentComponent : 'None'}}</h2>
                <!-- ATTN: Uncomment the keep-alive component to see what happens 
                    when you change the message in ChangeMessage component and toggle
                    back and forth from another component. -->

                <!-- <keep-alive> -->
                <component 
                    :is="currentComponent" 
                    :parentData="dataForChild" 
                    v-on:messageChanged="updateMessage">
                    <div class="slotData">This is a default slot</div>
                    <div slot="namedSlot" class="namedSlot">This is a NAMED slot</div>
                    <div slot="namedSlot" class="namedSlot"><p>Here we pass in the message via a slot rather than as a prop:</p>{{dataForChild.msg}}</div>
                </component>
                <!-- </keep-alive> -->
            </div>
        </div>
    </section>`,

    data() {
        return {
            currentComponent: false,
            /* You don't NEED to put msg and posts here, but
                I prefer it. It helps me keep track of what info
                my dynamic components need. */
            dataForChild: {
                // All components:
                msg: '', 
                
                // Async Component only
                posts: [] 
            }
        }
    },
    methods: {
        /**
         * Set the current component and the data it requires 
         *
         * @param {string} component The name of the component
         * @param {object} data The data object that will be passed to the child component
         */
        activateComponent(component, data = { posts: [], msg: 'This is a default msg.'}) {
            this.dataForChild = data;
            this.currentComponent = component;
        },
        deactivateComponent() {
            this.dataForChild.msg = '';
            this.currentComponent = false;
        },
        /* Hold off on loading the component until some async data is retrieved */
        getPosts() {
            axios.get('https://codepen.io/patrickodacre/pen/WOEXOX.js').then( resp => {
                const posts = resp.data.slice(0, 10) // get first 10 posts only.
                // activate the component ONLY when we have our results
                this.activateComponent('Async', {posts, msg: `Here are your posts.`})
            })
        },
        /** 
         * Update the message from the child
         *
         * @listens event:messageChanged
         * @param {string} newMessage The new message from the child component
         */
        updateMessage(newMessage) {
            this.dataForChild.msg = newMessage
        }
    },
    // must wire up your child components here
    components: {
        NoData,
        CustomMessage,
        DefaultMessage,
        Async,
        ChangeMessage
    }
} 

const routes = [
    { path: '/', name: 'home', component: Home}
];
    
const router = new VueRouter({
    routes
});
    
const app = new Vue({
    router
}).$mount("#app")

The html,

<div id="app">
    <h1>Vue.js Dynamic Components with Props, Events, Slots and Keep Alive</h1>
    <p>Each button loads a different component, dynamically.</p>
    <p>In the Home component, you may uncomment the 'keep-alive' component to see how things change with the 'ChangeMessage' component.</p>

    <nav class="mainNav"></nav>
    <!-- route outlet -->
    <!-- component matched by the route will render here -->
    <section class="mainBody">
        <router-view></router-view>
    </section>
</div>