Vue.js transition mode not working? - vue.js

I am currently working on a slider component but it seems that my transition mode is not working. I have used:
<transition name="fade" mode="out-in">
To my understanding this should force the element going out to first complete it's entire animation before the element in is being rendered.
This is an example of my slider component:
https://www.webpackbin.com/bins/-KfMMcLvpUA6LGOFL3vM
I could fix it by overlaying images absolute but that is obviously not something that I want to do when there could be a neater solution.
The transition CSS is included in the header of the index page:
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
I have it up the as easily usable as possible, if somebody could take a look that would be awesome.

Your understanding that one transition will complete before another starts is wrong. It might be true if you were changing out an element within a single transition tag, but it is not true when each element is wrapped in its own transition tag.
The easiest thing you could do is use setTimeout to make the assignment of the next activeSlide wait for the transition.
methods: {
prevSlide() {
const nextSlide = this.activeSlide === this.firstSlide ? this.lastSlide : this.activeSlide - 1;
this.activeSlide = null;
setTimeout(() => {
this.activeSlide = nextSlide;
}, 500);
},
nextSlide() {
const nextSlide = this.activeSlide === this.lastSlide ? this.firstSlide : this.activeSlide + 1;
this.activeSlide = null;
setTimeout(() => {
this.activeSlide = nextSlide;
}, 500);
}
}
That requires that your timeout value and your CSS transition length be kept in sync.
Somewhat more work would be to use the transition hooks to emit events when a leave transition started and ended.
<transition name="fade" mode="out-in" #leave="transitionStarted" #after-leave="transitionComplete">
You would catch them in the top-level code, (not in slider, because slot events don't propagate to the slot container)
<slide src="http://lorempixel.com/196/196" alt="" #transition-started="holdUp" #transition-complete="okGo"></slide>
In the handlers (holdUp and okGo) you would set a boolean data item that you would pass to slider as a prop. In slider, nextSlide and prevSlide would calculate what the next value of activeSlide will be, but will not set it. It would get set when the prop indicated that the transition was finished.

Related

Can't change transition on the fly for a transition group

In my app, clicking a modal's close button makes it disappear with a fade animation whereas swiping it down makes it disappear with a swipe animation. This is done by changing the modal's <transition name> based on event.
The same thing doesn't seem to work with a transition group. Am I doing something wrong, or is it actually not possible?
CodeSandbox
Template:
<transition-group :name="itemTransition">
<div
v-for="item in items"
:key="item.id"
v-hammer:swipe.up="() => onSwipeUp(notification.id)"
>
</div>
</transition-group>
Script:
export default {
data () {
return {
applySwipeTransition: false
}
},
computed: {
itemTransition () {
return this.applySwipeTransition ? 'swipe' : 'fade'
}
},
methods: {
onSwipeUp (id) {
this.applySwipeTransition = true
this.$nextTick(() => {
this.closeItem(id)
this.applySwipeTransition = false
})
}
}
}
CSS:
.fade-leave-active {
animation: fade-out .75s;
}
.swipe-leave-active {
animation: slide-up .25s;
}
The problem lies in the timing of component update. You are switching the transition mode back to fade in the same update cycle as when the element is closed. Thus, when the next component update is triggered (by removal of the item), the transition is already switched back to fade. At this point, you may have guessed that all that needs to be done, is to switch the transition back in the next update, triggered by removal of the item:
onSwipeUp (id) {
this.applySwipeTransition = true
this.$nextTick(() => {
this.closeItem(id)
this.$nextTick(()=>{
this.applySwipeTransition = false
})
})
}
Since there are no reasons to wait for component update to close the item, you can simplify the code a bit:
onSwipeUp (id) {
this.applySwipeTransition = true
this.closeItem(id)
this.$nextTick(() => {
this.applySwipeTransition = false
})
}
Here is your working sandbox: https://codesandbox.io/s/vue-template-forked-60lkk?file=/src/App.vue
So, I've worked around with your CSS by manually changing the name of the <transition-group to either fade or swipe to see if the there's a problem with the CSS animations.
Verdict: The fade works. swipe only transitions the list-item off the page by a click and drag, not true swipe, if that concerns you (by the way, my swipe is MacOS swipe - two-finger, no click)
Still, without changing the CodePen, the issue seems to be with your computed property where there's nothing telling the name to change dynamically even though you've bound it to a computed property - the logic for itemTransition() seems to always default to fade because the applySwipeTransition would never equal to "swipe", given that the CSS does work when you manually change name to swipe (see "Verdict)".
To see where the underlying issue was, I worked around with your itemTransition():
computed: {
itemTransition() {
return this.applySwipeTransition ? "fade" : "swipe";
},
Switching the order of the fade and swipe now makes swipe work. I hope this gives you some insight into the issue. You may need to create a custom Vue directive or event to handle the swipe / fade logic if needed.

Vue transition: how to slide correctly?

I have a v-card that I want to animate back and forth with one click.
If I click on an arrow to the left, the card should scroll to the right and then immediately scroll back in from the left. The other button should work the other way around.
The problem is, nothing happens here. What am I doing wrong?
My template:
<v-card>
<v-btn icon #click="back = false">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-btn icon #click="back = true">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</v-card>
<transition :name="back ? 'slide-fade' : 'slide-fade-reverse'">
<v-card max-width="200" class="mx-auto mt-5" height="80">
<span class="d-flex justify-center pt-7">{{back}}</span>
</v-card>
</transition>
My script:
data() {
return {
back: false,
}
},
My css:
/* Prev */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .3s ease;
}
.slide-fade-enter {
transform: translateX(100px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(-100px);
opacity: 0;
}
/* Next */
.slide-fade-reverse-enter-active {
transition: all .3s ease;
}
.slide-fade-reverse-leave-active {
transition: all .3s ease;
}
.slide-fade-reverse-enter {
transform: translateX(-100px);
opacity: 0;
}
.slide-fade-reverse-leave-to {
transform: translateX(100px);
opacity: 0;
}
I made a Pen for this: https://codepen.io/Tenarius/pen/WNwdEve
In order for leave and enter transition to work, the <transition> element has to have a v-if condition. When it changes from false to true, the element gets inserted into DOM and animates according to enter transition. When the condition changes from true to false, the leaving transition is performed and, when it ends, the element is removed from DOM.
However, you don't have such a condition. You're simply updating the cards contents and expect it to be removed from DOM and replaced by a new one.
In order to achieve the expected functionality you should use a list of cards (which would only contain the currently active card), coupled with using <transition-group> which, internally, uses the same mechanics as transition but the v-if condition is whether the element is part of the collection or not.
In your case, the "collection" would be a filtered list of cards, containing only one card. With this technique, the leaving element gets the leave animation, while the entering element gets the enter animation, as the elements are actually removed and added to DOM, according to changes in your model.
See it working here.
Since a transition needs leave and enter and thus the element has to "disappear" and "reappear", setTimout can be used to build a workaround.
data() {
return {
back: false,
loading: false
}
},
methods: {
loadTimeout() {
this.loading = true
setTimeout(function(){
this.loading = false
}.bind(this), 500);
}
}
The card can then be expanded with v-show="!loading" and the left- and right-buttons have to call the loadTimeout() function.
Working example here

How to disable transition-group only on page load?

I have a transition-group that renders a div, and within that is a component with a v-for attribute that renders several items. I then have a button that adds a new item to the beginning of the array. That transition works perfectly.
The only thing I don't like, is that the entire list loads with the transition on page load, and I'd like to disable it only on page load. I've searched Stack and Google but couldn't find a way. Is there a way to do this, so that transitions still works on button click, but is disabled for page load?
<transition-group
name="item-list"
tag="div">
<item-row
v-for="item in items"
:key="item.id"
:item="item" />
</transition-group>
.item-list-enter-active,
.item-list-leave-active,
.item-list-move {
transition : 250ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
transition-property: opacity, transform;
}
.item-list-enter {
opacity : 0;
transform: translateX(50px) scaleY(0.5);
}
.item-list-enter-to {
opacity : 1;
transform: translateX(0) scaleY(1);
}
.item-list-leave-active {
position: absolute;
}
.item-list-leave-to {
opacity : 0;
transform : scaleY(0);
transform-origin: center top;
}
I wish I could've found a more "Vue-y" way of handling this, however I ended up going this route. Essentially I added a class to the body and removed all transitions. Then on the created lifecycle of my component, I remove that class. This removes the transition on page load, but still keeps the transition on click of the button like I want.
You can dynamically change the name value of the transition-group. Maybe on page load have a value different from the value that has the correct class name that the CSS targets. Then in the mounted lifecycle hook you can change it back to the correct class name.
You need to bind the duration for transition-group
template:
<transition-group
:duration="duration"
name="item-list"
tag="div">
<item-row
v-for="item in items"
:key="item.id"
:item="item" />
</transition-group>
script:
data() {
return {
duration: 0,
items: [
{id: 1},
{id: 2}
]
}
},
methods: {
add() {
if(this.duration===0) this.duration = 250
this.items.push({id: 'xxx'})
}
}
In case anyone comes across this like I did.
I ended up achieving this by having a transitionsOn flag added to the data (didn't seem to matter what it was initialised to), and a computed name for the transition, i.e.
<transition-group :name="transitionName">
in computed, I then had, for a transition called 'flash'
computed: {
transitionName() {
return this.transitionsOn ? 'flash' : 'disabled';
},
},
I would then set this.transitionsOn = true when I wanted it to fire.
Took a lot of fiddling about to figure this out but it seems to work OK

Vue router in-out page transition: transition in a new route while old route remains visible

To illustrate what I'm trying to achieve but also discuss and learn about each mechanism separately, I split the issue into two independent challenges:
1. Keep previous route visible until new route has transitioned in
Whether the transition is sliding, what I'm trying here, or just fading; mode in-out doesn't work as I would expect it, namely that the existing route stays visible until the next route has finished its transition (e.g. overlaid itself over the previous one), exactly as illustrated here in the last example of this section https://v2.vuejs.org/v2/guide/transitions.html#Transition-Modes, showing two buttons with in-out mode. Instead no transition is happening but it just flips the routes statically at half of the given transition time.
Is there any caveat with routes and an obvious reason why this wouldn't work the same way, e.g. that a single router-view can only hold one at the time and therefore in-out is not possible?
EDIT 1:
I figured out that in-out would actually only work with position:absolute on both elements, otherwise they will not overlay. Any idea how I could elegantly include such a behavior, potentially setting that absolute position during router-transition only?
Current hack that has the visual slide-up modal effect (mode: in-out) I'm looking for: adding style="position:absolute; z-index:2100" to the dialog route. Then I would need to change the underlying transition once it's shown in order to have the reverse hide effect (mode: out-in).
Also see EDIT 2 below.
2. Creating a modal-like page (route) which opens above another existing page when navigated to
I tried to hack that behavior by adding a second router-view in App.vue
<router-view />
<router-view name="dialog" />
The particular component is added to my routes like this
{
path: 'records/new',
components: {
dialog: () => import('layouts/NewRecord.vue')
},
children: [
{
name: 'new-record',
path: '',
component: () =>
import('src/pages/NewRecord.vue')
}
]
}
I'm not sure whether this approach even makes sense but I couldn't make it work properly. The aim would be to just overlay another router-view name="dialog whenever a "dialog"-path is pushed, so while it can be animated (slide-up) the other router-view stays visible below. In the end I guess I'm facing the same issue here: once the route changes, the initial router-view discards its component because the path does not match the current location anymore.
Either way, there are people out there with more experience and expertise so I hope I could illustrate what I'm trying to achieve and I'm just curious and thankful to read your inputs.
EDIT 2
I could make it work the way I wanted with simply one , wrapped in a custom page-transition component. It is quite a hack though AND I needed to add position: absolute to may page-layouts, to all of them actually (both the "leaving" and the "entering" component need position: absolute) when showing the dialog component. I'm sure there's a better way but I haven't found it so far.
Custom page-transition component:
<template>
<transition :name="name" :mode="mode">
<slot/>
</transition>
</template>
<script lang="ts">
import { Component, Watch } from 'vue-property-decorator'
import Vue from 'vue'
import { Route } from 'vue-router'
#Component({
components: {}
})
export default class PageTransition extends Vue {
NAME_FADE = 'fade'
NAME_SLIDE_UP = 'slide-up'
NAME_SLIDE_DOWN = 'slide-down'
MODE_OUT_IN = ''
MODE_IN_OUT = 'in-out'
name = this.NAME_FADE
mode = this.MODE_OUT_IN
#Watch('$route', { immediate: true, deep: true })
onRouteChanged(newVal: Route, oldVal: Route) {
if (newVal.meta.transition === 'dialog') {
this.name = this.NAME_SLIDE_UP
this.mode = this.MODE_IN_OUT
} else if (oldVal && oldVal.meta.transition === 'dialog') {
this.name = this.NAME_SLIDE_DOWN
// shift next page in immediately below dialog
this.mode = this.MODE_IN_OUT
} else {
// default
this.name = this.NAME_FADE
this.mode = this.MODE_OUT_IN
}
}
}
</script>
<style lang="scss" scoped>
.fade-enter, .fade-leave-to {
opacity: 0;
}
.fade-enter-active, .fade-leave-active {
transition: all 0.1s ease;
}
// start of enter element
.slide-up-enter {
transform: translateY(60%);
opacity: 0;
}
.slide-up-enter-active {
transition: all 0.3s ease-out;
z-index: 2100;
}
// start of leave element
.slide-up-leave, .slide-up-leave-active {
opacity: 0;
}
// start of leave element
.slide-down-leave {
z-index: 2100;
}
.slide-down-leave-to {
transform: translateY(60%);
opacity: 0;
z-index: 2100;
}
.slide-down-leave-active {
transition: all 0.3s ease-in;
}
// start of enter element
.slide-down-enter {
opacity: 0;
}
.slide-down-enter-active {
/* show immediately behind existing page (lower z-index) */
transition: all 0s;
}
</style>
I have a similar task. I was able to complete it using fixed containers and z-index shuffle. I met a number of issues related to scroll and vertical alignment, and, in my case, solving it using absolute position during router-transition only was not possible.
Here's the demo: https://kasheftin.github.io/vue-router-in-out-slide-scroll.
Also, I had to use localStorage to keep & restore page scroll position.
In my case page content has to be vertically aligned. That's why I could not use one global scrollable container (e.g. <body>). In-out mode transition works rather simple - it just appends the content, adds some classes and then removes the first child. That means in the middle there're two page containers side by side, and if one of them is tall (and forces the body to have scroll), then the other one appears in the middle of the body and has wrong vertical alignment.
So I just wrapped every page with fixed scrollable container. Assume we have a List and an Item pages, and the last should slide from the right and overlay the list. Then, the right-to-left animation is very simple:
.slide-right-enter-active {
transition: transform 1s ease;
.slide-right-enter {
transform: translateX(100%);
}
Left-to-right animation (overlay disappearing) has the wrong z-index. During the animation we have the following in the DOM:
<transition>
<Item />
<List />
</transition>
By default List will be shown over the Item, but it has to be below. So there're the rules:
.slideable-page {
position: fixed;
overflow: auto;
z-index: 2;
}
.slide-left-enter {
z-index: 1;
}
.slide-left-enter-active {
z-index: 1;
}
.slide-left-leave-active {
transition: transform 1s ease;
z-index: 3;
}
.slide-left-leave-to {
transform: translateX(100%);
}
For question 1: Have you added the CSS with it? The transition by itself only handles timing, you need to add the CSS for the transition to work (example: https://v2.vuejs.org/v2/guide/transitions.html#Transitioning-Single-Elements-Components).
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
For question 2:
I don't know if I understood correctly your situation, but if I did, here is what I would do, using nested routes.
layouts/NewRecord.vue
<template>
<router-view name="dialog"></dialog>
</template>
Routes
const routes = {
path: 'records/new',
component: () => import('layouts/NewRecord.vue'),
children: [
{
path: 'dialog',
components: {
dialog: () => import('src/pages/NewRecord.vue'),
},
},
],
}

Can the <transition> element be used to animate individual page elements in a nuxtjs application?

Can someone tell me if the transition element can be used on page elements for animations in nuxt? I have seen the doc regarding page transitions, but I want to animate a number of different page elements. What I have so far does not appear to be working.
In a simple Header component, I have this:
<template>
<transition name="menu-popover">
<ul class="MenuPopover">
<li>Payments</li>
<li>Subscriptions</li>
<li>Connect</li>
</ul>
</transition>
And in the style tag of that component:
<style scoped>
.menu-popover-enter {
opacity: 0;
transform: rotateY(50deg);
}
.menu-popover-enter-to {
opacity: 1;
transform: rotateY(0deg);
}
.menu-popover-enter-active {
transition: opacity, transform 200ms ease-out;
}
Solution 1:
Look into the Nuxt Guide: Page Transition, it introduces how to implement the transition for each page (or specific pages Nuxt API: Page Transition) step by step very well.
Solution 2 (not recommend, but if really prefer to uses <nuxt /> inside one <transition> manually):
Steps:
put <nuxt> inside <transition>, like <transition name="test"><nuxt v-show="pageShow"/></transition>
add css class for transition effects,
css will be like:
.test-enter {
opacity: 0;
transform: rotateY(50deg);
}
.test-leave-to {
opacity: 0;
transform: rotateY(100deg);
}
.test-enter-active,.test-leave-active {
transition: all 2s ease-out;
}
add one handler for router navigator (or like button click event which will trigger route change).
The handler will be like below:
changePage: function (newUrl) {
this.pageShow = false //hide current page to trigger the transtion for `leave` current page
setTimeout(()=> {
this.pageShow = true //show new page, it will trigger the transition for `enter` new page
this.$router.replace(newUrl) //with new url
}, 2000) // delay 2s (after the transition of previous page finishes)
}