Vue transitioning effects disappear when inside a vue slot - vue.js

I'm having a hard time getting a vue transition to work inside a slot. I should mention I've searched through numerous stackoverflow questions and I haven't seen one that matches my issue. All the ones I've seen so far are talking about <slot> inside the <transition>, eg: vue transition component doesn't work correctly, whereas for me it's the other way round.
There are three vue components, component1, component2 and component3.
component1 has something like:
<template>
<slot name="content">
<slot name="side">
<slot name="default">
</template>
component2 looks something like below:
<component1>
<template #content>Some stuff here</template>
<template #side>Some stuff here</template>
<template #default>
<component3/> #Component3 contains the transition stuff
</template>
</component1>
component3 contains the transition stuff:
<transition
:name="right ? 'slide-right': 'slide-left'"
#before-enter="$emit('before-enter')"
#after-leave="$emit('after-leave')"
>
<div
v-show="$attrs.value"
v-bind="$attrs"
v-on="listeners"
class="u-drawer bg-white"
:class="extraClasses"
:style="getStyles">
<slot v-if="!hideClose" name="close">
<u-button #click="$emit('input', false)"
class="u-drawer-close-button" icon>
<u-icon icon="x" size="1rem" class="mx-auto" />
</u-button>
</slot>
<slot name="default" />
</div>
</transition>
<transition name="fade">
<div v-if="overlay" #click.stop="closeDrawer" v-show="$attrs.value" style="width:100%; height:100%" class="u-overlay"></div>
</transition>
For whatever reason the transition does not work, it's a drawer that is supposed to slide out but it doesn't, instead it just appears instantaneously.
I should mention that, component3 has been used elsewhere in the project where the sliding effect does occur. The only difference here is that it's within a <slot name=default>. Does anyone know what the problem might be?

Related

Nuxt2/Vue2, checking a $slots sometime fails on rerender

I have a strange issue on a Nuxt2/Vue2 project.
I have a component with a slot named "image" conditionally displayed. The condition only checks "$slots.image". It works properly on the first render of the component, but if it have to rerender, sometimes the condition fail.
I fixed this issue by modifying the condition to checks also "$scopedSlots.image". But I don't understand what is happening.
Does someone can explain to me what is happening here ?
Here is the parent where my component is imported:
<template>
<div>
<nav-pane>
<template #subheader>
<nav-pane-sub-header>
<template #image>
<avatar />
</template>
</nav-pane-sub-header>
</template>
</nav-pane>
</div>
</template>
Here is my component 'NavPaneSubHeader' with the buggy condition:
<template>
<div>
<div>
<i v-if="icon" />
<slot v-else-if="$slots.image"/>
</div>
<slot />
</div>
</template>
Works fine with :
<slot v-else-if="$scopedSlots.image || $slots.image" />

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 transition works on router-view but not inside component

When I setup a typical route transition everything works as expected
<transition name="view" mode="out-in">
<router-view />
</transition>
But if I try to put the transition inside a view instead, the transition doesn't work
<template>
<transition name="view" mode="out-in">
<main>
<...>
</main>
</transition>
</template>
Any ideas why this could be the case?
The issue, seems to me, is that the <main> element is not entering or leaving, so it doesn't animate. You're probably trying to animate something within the <main> element which does not get targeted by the <transition> pseudo-element.
my suggestion is to encapsulate the element that's being toggled (v-if) or having visibility toggled (v-show) with transition
<template>
<main>
<transition name="view" mode="out-in">
<...something with a v-if or a v-show>
</transition>
</main>
</template>
Also, if you have are using a list/array (v-for) you should use transition-group
docs: https://v2.vuejs.org/v2/guide/transitions.html

Vuejs nested slots: how to pass slot to grandchild

I use different vuetify components, for example v-menu. It has a template like this:
<v-menu>
<a slot="activator">menu</a>
<v-list>
<v-list-tile>Menu Entry 1</v-list-tile>
<v-list-tile>Menu Entry 2</v-list-tile>
</v-list>
</v-menu>
Suppose I want to add another wrapper around it. That is my special menu component that has some predefined menu options. And I want it to has an activator slot as well. And the last should be somehow assigned to the original v-menu activator slot. Is it possible?
Example:
// index.vue:
<template>
<my-special-menu>
<button>My special menu trigger</button>
</my-special-menu>
</template>
// MySpecialMenu.vue
<template>
<v-menu>
<slot slot="activator"/> <-- I don't know how to write this line
<v-list>...</v-list>
</v-menu>
</template>
<slot slot="activator"> is an incorrect equation. The goal is to pull the content from the parent (that is <button>..</button> in the example), and use it as slot="activator" in v-menu.
I can write it like this:
<v-menu>
<a slot="activator"><slot/></a>
...
</v-menu>
But this case the result template will be:
<div class="v-menu__activator">
<a>
<button>My special menu trigger</button>
</a>
</div>
That's not exactly what I want. Is it possible to get rid off <a> wrapper here?
Update:
We can use a construction like <template slot="activator"><slot name="activator"/></template> to throw some slot to a grand child. But what if we have multiple slots and we want to proxy them all? That's like inheritAttrs and v-bind="$attrs" for slots. Is it currently possible?
For example, there's <v-autocomplete> component in vuetify that has append, prepend, label, no-data, progress, item, selection etc slots. I write some wrapper component around this, it currently looks like:
<template>
<v-autocomplete ..>
<template slot="append"><slot name="append"/></template>
<template slot="prepend"><slot name="prepend"/></template>
<template slot="label"><slot name="label"/></template>
...
<template slot="item" slot-scope="props"><slot name="item" v-bind="props"/></template>
</v-autocomplete>
</template>
Is it possible to avoid all slots enumeration here?
If you put the slot attribute on a html element, that html element is passed to the child component to fill the slot with that name. If you don't want to pass along a html element, you can use slot on a template tag within your component. A template tag groups elements, but does not render to a html element, which is perfect here. You can use template tags also for other things, such as to group elements in a v-if for example, or to repeat multiple elements with a v-for.
// App.vue
<template>
<div id="app">
<test>
<template slot="activator">
Click <b>me</b>!
</template>
</test>
</div>
</template>
// Test.vue
<template>
<div class="wrapper">
<grand-child>
<template slot="activator">
<slot name="activator"></slot>
</template>
</grand-child>
This is some text
</div>
</template>
// GrandChild.vue
<template>
<div>
<a href="#" #click="toggle = !toggle">
<slot name="activator">Default</slot>
</a>
<div v-if="toggle">This appears and disappears</div>
</div>
</template>
Edit: If you want to do this for arbitrary slots, this is also possible. this.$slots contains the slots and their content, so with something like the following, you can pass the slot content to a slot with the same name:
<grand-child>
<template v-for="(_, slot) in $slots">
<template :slot="slot">
<slot :name="slot"></slot>
</template>
</template>
</grand-child>
For completeness sake, scoped slots can be accessed through $scopedSlots and be propagated like so:
<grand-child>
<template v-for="(_, slot) in $scopedSlots" v-slot:[slot]="props">
<slot :name="slot" v-bind="props" />
</template>
</grand-child>
source and comment
I had EsLint errors because of the depreciated :slot and $scopedSlots attributes.
So I combined both of #Sumurai8 answers like this and it works great:
<template v-for="(_, slot) in $slots" v-slot:[slot]>
<slot :name="slot"></slot>
</template>
If you have both named and unnamed slots with props:
Vue 3
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}" />
</template>
Typescript version
<template v-for="(_, name) in ($slots as {})" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}" />
</template>

How to manage dialogs in different router-views?

I've got a problem with my vue-router and v-dialogs (vuetify.js components) inside different router-views.
My App.vue looks like this:
<template>
<transition>
<keep-alive>
<router-view/>
</keep-alive>
</transition>
</template>
Inside each router-view I have a component:
<template>
<v-app>
<app-header></app-header>
<v-content></v-content>
<app-footer></app-footer>
<!-- a couple of dialogs here -->
<v-dialog v-model="someData.show"></v-dialog>
<v-dialog v-model="anotherData.show"></v-dialog>
<v-dialog v-model="yetAnotherData.show"></v-dialog>
</v-app>
</template>
When I switch between different router-views and open dialogs overlay shows, but the dialog doesn't. Except for the router-view which was first loaded with the page.
What seems to be the problem? Does is have something to do with the way I define there routes or components?
You check full code over here: https://github.com/websanya/gkp7-app/
Please, help!
In fact I should've used only one v-app outside of router-view in App.vue file.
<template>
<!-- OVER HERE -->
<v-app>
<transition>
<keep-alive>
<router-view/>
</keep-alive>
</transition>
</v-app>
<!-- / OVER HERE -->
</template>
Works like a charm.