Vue: How to trigger transitions for dynamic components instances that use the same template - vue.js

I'm using Vue 3 with tailwind and I'm having trouble transitioning between dynamic components. As described in the documentation, I tried the following:
<transition
enter-active-class="duration-500 ease-out"
enter-class="translate-x-full opacity-0"
enter-to-class="translate-x-0 opacity-100"
leave-active-class="duration-500 ease-in"
leave-class="translate-x-0 opacity-100"
leave-to-class="translate-x-full opacity-0"
mode="out-in"
>
<component :is="elementFormat" :key="elementKey" />
</transition>
Where elementFormat obviously refers to the component template that should be rendered. This is a computed property from the vuex store.
Although I can see that elementFormat is updated in the console, the transition won't play, unless I wrap a v-if or v-show around it, and hide it. What am I missing?

Check if you have imported the tailwind css correctly because I have recreated your problem and it seems everything works perfectly,
const Demo = {
data() {
return {
view: 'v-a'
}
},
components: {
'v-a': {
template: '<div>Component A</div>'
},
'v-b': {
template: '<div>Component B</div>'
}
}
}
Vue.createApp(Demo).mount('#demo')
.component-fade-enter-active,
.component-fade-leave-active {
transition: opacity 0.3s ease;
}
.component-fade-enter-from,
.component-fade-leave-to {
opacity: 0;
}
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue#next"></script>
<div id="demo">
<h3>Vue.js Demo</h3>
<input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label>
<input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label>
<transition name="component-fade" mode="out-in">
<component :is="view"></component>
</transition>
<br>
<br>
<h3>Your Code</h3>
<transition enter-active-class="duration-500 ease-out" enter-class="translate-x-full opacity-0" enter-to-class="translate-x-0 opacity-100" leave-active-class="duration-500 ease-in" leave-class="translate-x-0 opacity-100" leave-to-class="translate-x-full opacity-0"
mode="out-in">
<component :is="view" />
</transition>
</div>

Related

vuejs transitions only on some views

I have transitions working between the pages of my vuejs application, defined in App.vue like so:
<template>
<div class="container mb-auto">
<router-view v-slot="{Component}" >
<transition name="slide" mode="out-in">
<component :is="Component" :key="route.path"></component>
</transition>
</router-view>
</div>
<TheFooter v-if="withMenu" />
</template>
// and definition of transitions in css
I don't want this to work between all views (pages) of my app, but only between views who's url starts with /welcome
How do I use some transitions between some pages, and other transitions between other pages?
You can use the JS transitions hooks as shown here: https://vuejs.org/guide/built-ins/transition.html#javascript-hooks
And make a check if you're on the correct path or not, example on a /welcome-home path below
<template>
<div>
<transition #before-enter="onBeforeEnter">
<!-- ... -->
</transition>
</div>
</template>
<script>
export default {
methods: {
onBeforeEnter() {
if (this.$route.path.startsWith('/welcome')) {
// cool transitions!
}
},
},
}
</script>

The opposite of ‘appear’ for Vue transitions?

I’m trying to create a ‘modal’ component. It’s using a ‘v-if’ attribute for deciding whether the component is shown or not.
The Vue docs mention you can use ‘appear’ for transitions (https://vuejs.org/guide/built-ins/transition.html#transition-on-appear), which works well.
However, when I close the modal, the element gets removed from the DOM, and the ‘leave’ transition is not trigged.
Is there an opposite of ‘appear’ for Vue transitions? How can I apply a leave transition when an element is removed from the DOM?
Link of example: https://jsfiddle.net/jelledv4/3Lr79hyg/4/
<transition
appear
enter-active-class="transition duration-1000 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-1000 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
PS I could use ‘v-show’ instead of ‘v-if’ to mitigate this, but for this use-case ‘v-if’ suits better. Plus I’m wondering how I can tackle this issue
In your provided jsfiddle, you use v-if on the parent element of the Transition components. Instead, use v-if on the child/slot of the Transition component. Otherwise, the transition components will be removed immediately.
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
toggle() {
this.show = !this.show;
},
}
});
app.mount('#app');
<div id="app">
<button #click="toggle" class="fixed z-10 bg-white ml-3 mt-3 border border-gray-800">Toggle</button>
<div>
<!-- Modal background -->
<transition
appear
enter-active-class="transition duration-1000 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-1000 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="show" class="absolute left-0 top-0 w-full h-full bg-black bg-opacity-50"></div>
</transition>
<!-- Modal content -->
<transition
appear
enter-active-class="transition duration-1000 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-1000 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<div v-if="show" class="fixed bg-red-500 w-52 h-52 z-10 mt-14">
<h2 >test</h2>
</div>
</transition>
</div>
</div>
<script src="https://unpkg.com/vue#3"></script>
<script src="https://cdn.tailwindcss.com"></script>

Vue transition works for "enter" state but not for "leave" state

I have a modal rendered on top of a semi-transparent backdrop. Both elements have a v-if controlled by the same variable.
Although the enter transition animation works fine, the `leave`` transition animation is ignored (it should fade out smoothly, instead it disappears instantly). Why?
Codepen
Markup:
<div id="app">
<button #click="showModal = !showModal">Toggle Modal</button>
<div v-if="showModal" class="modalBackdrop">
<transition name="content" appear>
<div v-if="showModal" class="modalContent">
Modal
</div>
</transition>
</div>
</div>
CSS:
.content-enter-active {
animation: slide-up .75s;
}
.content-leave-active {
animation: fade-out .75s;
}
#keyframes slide-up {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
#keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
It seems that the div with modalBackdrop class is disappearing before the nested div with class modalContent does its transition, so try to wrap modal Backdrop by a transition component with name backdrop which also takes the fade-out animation :
.backdrop-leave-active,.content-leave-active { /*.backdrop-leave-active is sufficient since the parent opacity is applied on children*/
animation: fade-out .75s;
}
template :
<div id="app">
<button #click="showModal = !showModal">Toggle Modal</button>
<transition name="backdrop" appear>
<div v-if="showModal" class="modalBackdrop">
<transition name="content" appear>
<div v-if="showModal" class="modalContent">
Modal
</div>
</transition>
</div>
</transition>
</div>
DEMO
When showModal is false, the transition element is destroyed immediately. If the only reason you use v-if="showModal" in transition parent is to disable modalBackdrop, then you can assign this class dynamically.
This is working as expected:
<div :class="{ modalBackdrop: showModal }">
<transition name="content" appear>
<div v-if="showModal" class="modalContent">
Modal
</div>
</transition>
</div>

Why does this Vue3 transition break data binding?

I have this issue I've been hitting for hours now; I can't understand why it doesn't work as expected.
I pasted an example code below. The issue is that when editing the name, {{name}} is not updated. However, if I remove either of the <transition> element or the v-if="show" condition, then data binding works as expected. Same if the {{name}} is placed outside the transition.
So it seems the transition blocks data binding? However I don't find anything about it in the docs or elsewere. I tested this code in a Vue2 playground, and it works as expected (data binding works). So the behavior seems to depend on Vue3.
Is there something I'm missing? Is it a bug in Vue3?
Thanks in advance for any input or idea.
<template>
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<div v-if="show">
<p>hello, {{name}}</p>
<input v-model="name" type="text" />
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
name: "",
show: true,
}
}
});
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.8s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
It works just fine in plain JS...
So try to focus on the differences:
TypeScript (i cannot use it here on SO) - I really doubt its the cause but you can try
Scoped CSS - did you tried to remove scoped ? There are some issues with scoped CSS and <transition>. Check this issue in Vue-loader. My example is not build with Webpack so Vue-loader is not used but it's for sure used in your project...
const app = Vue.createApp({
data() {
return {
name: "",
show: true,
}
},
template: `
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<div v-if="show">
<p>hello, {{name}}</p>
<input v-model="name" type="text" />
</div>
</transition>
</div>
`
}).mount("#app");
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.8s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app"></div>
I meet same question, you can try to set the initial value of 'show' to false and at the lifeCycle to modify 'show' for true.

Nesting a slot in a slot for vue

Update: Here's a simplified version of what I'm trying to achieve here (from the threaded conversation below):
Accept Component A - Accept Component B - Accept a condition - if
condition is true : wrap Component B with Component A [and render]- else only
render component B.
I'm interested in creating a component that renders a wrapper conditionally. I figured a theoretical approach like this would probably be best**:**
<template>
<div>
<slot v-if="wrapIf" name="wrapper">
<slot name="content"></slot>
</slot>
<slot v-else name="content"></slot>
</div>
</template>
<script>
export default {
props: {
wrapIf: Boolean,
}
}
</script>
Then when we implement, it would look something like this:
...
<wrapper-if :wrap-if="!!link">
<a :href="link" slot="wrapper"><slot></slot></a>
<template slot="content">
content
</template>
</wrapper-if>
The idea being that, in this case, if there is a link, then let's wrap the content with the wrapper slot (which can be any component/element). If there isn't, then let's just render the content without the wrapped link. Pretty simple logic, but it seems that I'm misunderstanding some basic vue functionality because this particular example does not work.
What is wrong with my code or is there some kind of native api that already achieves this or perhaps a dependency that does this sort of thing already?
The output should look like this:
wrapIf === true
<a href="some.link">
content
</a>
wrapIf === false
content
Just focus on the content itself, and let the component worry about whether or not to wrap the default or named content slot.
If you need the wrapper to be dynamic, a dynamic component should solve that. I've updated my solution accordingly. So if you need the wrapper to be a label element, just set the tag property to it, and so on and so forth.
const WrapperIf = Vue.extend({
template: `
<div>
<component :is="tag" v-if="wrapIf" class="wrapper">
<slot name="content"></slot>
</component>
<slot v-else name="content"></slot>
</div>
`,
props: ['wrapIf', 'tag']
});
new Vue({
el: '#app',
data() {
return {
link: 'https://stackoverflow.com/company',
tagList: ['p', 'label'],
tag: 'p',
wrap: true
}
},
components: {
WrapperIf
}
})
.wrapper {
display: block;
padding: 10px;
}
p.wrapper {
background-color: lightgray;
}
label.wrapper {
background-color: lavender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<wrapper-if :wrap-if="wrap" :tag="tag">
<a :href="link" slot="content">
content
</a>
</wrapper-if>
<div>
Change wrapper type:
<select v-model="tag">
<option v-for="tag in tagList">{{tag}}</option>
</select>
</div>
<button #click="wrap = !wrap">Toggle wrapper</button>
</div>