Search code examples
vue.jslayoutnamedv-slot

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.

enter image description here

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?


Solution

  • 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>