Register multiple Vue components in parent component - vuejs2

I have a global sidebar component TheSidebar.vue:
<template>
<aside>
<slot></slot>
</aside>
</template>
In Blogs.vue (a page component) I try to register two components.
<template>
<div>
<h1>Experiences</h1>
<TheSidebar>
<SearchBlog />
<CategoryCheckboxFilter />
</TheSidebar>
<ExperienceList />
</div>
</template>
It seems like I cannot register two components in a slot?
Is this a good setup anyway and who do I have to achieve this?
Update
It's just working fine now and I can register more than one component in a <slot />. I think some webpack building issue before.

Related

how to delegate content from vue slots to web components slots?

The Problem
Let's say we have a page template written as a Web component in a shared library to keep the company design system consistent. That page has some slots:
export class PageTemplate extends LitElement {
static properties = {
title: { type: String },
};
render() {
return html`
<div>
<h1>${title}</h1>
<slot name="template-body"></slot>
<div class="some-special-styles">
<slot name="template-buttons"></slot>
</div>
</div>
`;
}
}
customElements.define("page-template", PageTemplate);
Then we use this template in a Vue (v3.2.45) application on a base component to be used in the same app by multiple pages.
//page-base.vue
<template>
<page-template title="My App Name">
<slot name="base-body"></slot>
<slot name="base-buttons"></slot>
</page-template>
</template>
Here, we will use the page base vue component on a specific page.
//login-page.vue
<template>
<PageBase>
<template #base-body>
<div slot="template-body">
<input placeholder="some special code"/>
</div>
</template>
<template #base-buttons>
<button slot="template-buttons">login</button>
<button slot="template-buttons">back</button>
</template>
</PageBase>
</template>
To make the login page components show inside that original page template web component; we need to declare the slot property on the leaf components like in <button slot="template-buttons">
How can I implement the Vue Page Base component to avoid the need to remember to set the slot property in every leaf vue component?
Things I've Tryied
I've tried to solve this using the vanilla web syntax below, but Vue appears not to dispatch that information to the final HTML:
//page-base.vue
<template>
<page-template title="My App Name">
<!-- this does not work -->
<slot name="base-body" slot="template-body"></slot>
<slot name="base-buttons" slot="template-buttons"></slot>
</page-template>
</template>
There was also an attempt (after a suggestion in the comments) to use a template as a ghost intermediate in the page base. But nothing was rendered at runtime.
//page-base.vue
<template>
<page-template title="My App Name">
<!-- i can't have that span because of some-special-styles applied in the template-->
<template slot="template-body"><slot name="base-body"></slot></template>
<template slot="template-buttons"><slot name="base-buttons"></slot></template>
</page-template>
</template>
The approach to using some middle element to make the connection (like below) enables content rendering. Still, it does not work for the project requirements because, for style reasons, I need that the final components be the top-most nodes in the page template slots.
//page-base.vue
<template>
<page-template title="My App Name">
<!-- although it runs, i can't have these spans because of some-special-styles applied in the template -->
<span slot="template-body"><slot name="base-body"></slot></span>
<span slot="template-buttons"><slot name="base-buttons"></slot></span>
</page-template>
</template>

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.
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?
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>

Vue 3 – <Transition> renders non-element root node that cannot be animated

App.vue has a transition tag to fade the pages out and in.
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in" appear>
<component :is="Component"></component>
</transition>
</router-view>
The Page.vue file has a simple structure, however, it also has a basic sliderjs component which throws the error <Transition> renders non-element root node that cannot be animated. If the transition tag is removed, everything works fine.
<div v-if="page.isReady">
<swiper>
<swiper-slide>Slide 1</swiper-slide>
<swiper-slide>Slide 2</swiper-slide>
<swiper-slide>Slide 3</swiper-slide>
</swiper>
</div>
https://swiperjs.com/vue/
The file also has the following:
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/swiper.scss';
export default {
components: {
Swiper,
SwiperSlide,
},
setup () {
return {
page: usePage()
}
}
}
Is there any trick to fix the error? Thanks for any tips!
No.
<template>
<div></div>
<div>~someone~</div>
</template>
Yes.
<template>
<div>
  <div></div>
~someone~
</div>
</template>
If you do not use a "div" tag just inside the "Template" tag, you will get the same error. (By the way, it was possible to use other than div tags)
Transitions require single children nodes. Therefore you can wrap the <component> tag inside a <div>, however, a plain <div> inside a <transition> won't trigger the transition, but changing the key attribute does.
We can obtain a unique key by getting the route name:
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<div :key="route.name">
<component :is="Component"></component>
</div>
</transition>
</router-view>
This will effectively transition between routes with a different name, but if you also want to transition between routes of the same name with different parameters, you can use route.fullPath instead of route.name as the key.
I can't fully take credit for this one...but I was having a similar issue and the problem was I had multiple nodes in my view, and found this guy's post on the Vue.js forums:
Found my mistake too. Transition required a single root in components! Since Vue 3 no longer requires a single root node for components I thought this also applies to transitions.
But it’s also logical. CSS requires a single root to which the transitions can refer.
When toggling between elements that have the same tag name, you must tell Vue that they are distinct elements by giving them unique key attributes. Otherwise, Vue’s compiler will only replace the content of the element for efficiency. Even when technically unnecessary though, it’s considered good practice to always key multiple items within a component.
<div>
<router-link to="/"></router-link>
<router-link to="/about"></router-link>
</div>
<router-view v-slot="{ Component, route }">
<transition name="route" :key="route" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
I solved it, it was a minor mistake, there was a character outside a html tag, directly after the tag (comma).
<template>,
<div>
<div>
<swiper>
<swiper-slide>Slide 1</swiper-slide>
<swiper-slide>Slide 2</swiper-slide>
<swiper-slide>Slide 3</swiper-slide>
</swiper>
</div>
</div>
</template>
your dynamic component instance must have a root element.
in you example,'Swiper' and 'SwiperSlide' must have a root element!
don't use RouterView in component parameter of the router. if you need to do that put it inside a root element
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in" appear>
<!-- root element -->
<div>
<component :is="Component"></component>
</div>
</transition>
</router-view>
A solution that worked for me using Nuxt3:
Parent page was:
<template>
<NuxtPage />
</template>
but should be also wrapped into a root node:
<template>
<div>
<NuxtPage />
</div>
</template>
Otherwise I got a transition exception:
Component inside <Transition> renders non-element root node that cannot be animated
I'm doing like this and it works.
<template>
<router-view v-slot="slotProps">
<transition name="route" mode="out-in">
<component :is="slotProps.Component"></component>
</transition>
</router-view>
</template>

vue emit data back to parent when using a slot

I have input on custom component and when i click on the next button on the wrapper component i need to emit details to the parent component.
How is this possible in vue?
wrapper.vue
<template>
<div :id="contentId" class="mt-3">
<div>
<slot></slot>
</div>
<b-row class="float-right">
<b-col>
<button cssClass="e-outline" v-on:click="btnNext">{{nextButtonText}}</button>
</b-col>
</b-row>
</div>
</template>
parent.vue
<template>
<div>
<Wrapper contentId="1">
<CustomComponent1 />
</wrapper>
<Wrapper contentId="2">
<CustomComponent1 />
</wrapper>
</div>
</template>
customComponent1.vue
<template>
<div>
<input v-model="name" />
<input v-model="name2" />
</div>
</template>
code above is for illustrative purposes.
The problem is that the wrapper doesn't innately have access to data of the scoped component, therefore these links have to be created manually. There is no way to tell how many children or slots the component may have, so this kind of functionality is not part of the vue magic.
So in an example where you have parent App component, which holds a Wrapper that has a MyInput component in the slot...
MyInput
The MyInout component doesn't automatically update other components, so it needs to be setup to $emit the internal data.
This can be done using a watch, #change listener for the input, or some other way. You can emit multiple datum as they change, or use a single payload with all the data
this.$emit("input", myData);
App
The App needs to explicitly connect the data between MyInout and Wrapper
<Wrapper> <MyInput #input="onInput" slot-scope="{ onInput }" /> </Wrapper>
The magic/trick happens here, where we bind the input emit function of the input to the onInput function using slot-scope.
Wrapper
The wrapper then needs to listen to the events passed (via App) from Wrapper
<slot :onInput="onInput" />
where onInput is a method that would process the data
see example below
I would recommend the following reading
https://github.com/vuejs/vue/issues/4332 (specifically Evan's response why it's not possible)
https://adamwathan.me/renderless-components-in-vuejs/ Adam has a thoroughly documented way of using render functions and slots to abstract functionality from the UI. While it's not directly related, it's a worthwhile read and may provide more info on using slot-scope as well as some perspective on improving the structure of UI components.

VUE same component instance at difference route

I am design an Vuejs app which page render based on route.
e.g. for
route = /, Component = Main.vue
<template>
<div>
<toolbar :user="user"></toolbar>
<app-content></app-content>
</div>
</template>
route = /:user, Component = User.vue
<template>
<div>
<toolbar :user="user"></toolbar>
<userHeader></userHeader>
<app-content></app-content>
</div>
</template>
When the page is show, the toolbar component will fetch data from server, the problem is, when the page go from / to /user, the data fetching data X 2 because that are 2 toolbar components in the app itself.
How should resolve this issue ? is that any way to reuse share component instances like toolbar ?
or should i put the design in one whole component instead ? ( use v-if to show hide the additional component)
You should be having <toolbar /> outside of <router-view></router-view>.
So your code should look like:
<div id="app">
<toolbar user="user" />
<router-view />
</div>
With this <toolbar /> won't change even if you change your routes, and will result in data fetching for a single time only.