Vue transition on router - but transition effects specific html Element - vue.js

I have a page transition for VUE js that I have implemented. I did this manually because I could not find how to do this using VUES transition.
(I am using gridsome framework for vue js - I have added a custom App.vue page - which should allow transitions of gridsome to act like normal Vue js transitions)
I feel like what I have done is bloated for its use case so wanted to see if anyone knew how to implement this using vue transtions.
#1
Users click component (which has a #click - triggering a this.$router.push() to the route)
#2
A div pops over the screen in the color of that component, creating a nice fade to hide the transition
#3
On the new page, another div identical to the transition one, now exits the screen.
I have this working here for reference, just click on clients (please try not to judge me to much, its still in development) -
https://wtwd.ninjashotgunbear.com/
MY METHOD:
Index.html
Each component is a SectionTitle when the user clicks on one of them they $emit the specific obj with the data for that page (such as the color && the name of the page to be routed to) - this is the #routeChange="reRoute($event) seen below:
<template>
<Layout>
<div class="navs" v-for="section in sections" :key="section.sectionTitle">
<!-- On click delay for screen to come ove top -->
<!-- router to be put here -->
<SectionTitle :data="section" #routeChange="reRoute($event)"/> <<<< COMPONENT that $emits on click
</div>
<!-- fullpage div to slide in and cover up no leave transition -->
<div class="leaveScreen"></div> <<<<< DIV that covers the screen
</Layout>
</template>
This triggers my method that moves the div over the UI view and creates the transition effect:
methods:{
reRoute(value){
console.log(value)
// 1) animate the loading screen
let screen = document.querySelector('.leaveScreen');
screen.style.cssText=`background: ${value.backgroundColor}; left: 0%`;
// 2) re-route the page
setTimeout(()=>{
this.$router.push(value.sectionLink)
}, 700)
}
}
CSS FOR DIV :
.leaveScreen {
position: absolute;
top: 0;
bottom: 0;
left: -100%;
width: 100%;
z-index: 11;
// background color added by the fn reRoute()
transition: all 0.7s;
}
The on the page, I use the mounted hook to remove the div from the users view (in the same, but other way around, way that I added it above.
mounted(){
let screen = document.querySelector('.fadeOutScreen');
// set timeout works to delay
setTimeout(()=>{
screen.style.cssText='left: 100%;'
},700)
}
If you know how to do this in a cleaner code / or by using VUES transition property then your help is very welcomed. I figured that VUE would have a specific way of doing this, but have not found it yet.
Thanks in advance -
W

If you wrap .leave-screen in a transition element you can do something like this:
new Vue({
el: "#app",
data: {
leaveScreen: false
}
})
body {
margin: 0;
}
.click-me {
cursor: pointer;
font-size: 30px;
}
.leave-screen {
position: absolute;
height: 100vh;
width: 100vw;
top: 0;
background-color: rgb(0, 0, 0);
}
.leave-screen-enter-active,
.leave-screen-leave-active {
background-color: rgba(0, 0, 0, 1);
transform: translateX(0);
transition: all 1s ease-in-out;
}
.leave-screen-leave-to,
.leave-screen-enter {
background-color: rgba(0, 0, 0, 0);
transform: translateX(-100%);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div #click="leaveScreen = true" class="click-me">
Click Me
</div>
<transition name="leave-screen">
<div v-if="leaveScreen" class="leave-screen" #click="leaveScreen = false"></div>
</transition>
</div>
.leave-screen-enter-active and .leave-screen-leave-active define the state of the element during transition.
.leave-screen-leave-to is the state the element leaves to (surprisingly) and .leave-screen-enter is the state of the element before it enters.
The styles you set on the element itself are where the transition starts/ends (depending on whether it's entering/leaving).
Vue's definitions:
v-enter: Starting state for enter. Added before element is inserted, removed one frame after element is inserted.
v-enter-active: Active state for enter. Applied during the entire entering phase. Added before element is inserted, removed when transition/animation finishes. This class can be used to define the duration, delay and easing curve for the entering transition.
v-leave-active: Active state for leave. Applied during the entire leaving phase. Added immediately when leave transition is triggered, removed when the transition/animation finishes. This class can be used to define the duration, delay and easing curve for the leaving transition.
v-leave-to: Only available in versions 2.1.8+. Ending state for leave. Added one frame after a leaving transition is triggered (at the same time v-leave is removed), removed when the transition/animation finishes.

Related

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'),
},
},
],
}

Vue.js router transition fade gap

I'm using the amazing transition to slide router pages in vue.js
<template>
<div>
<header-comp></header-comp>
<transition
name="custom-classes-transition"
mode="out-in"
enter-active-class="animated slideInLeft"
leave-active-class="animated slideOutRight"
>
<router-view></router-view>
</transition>
<footer-comp></footer-comp>
</div>
</template>
<style>
#import 'https://cdn.jsdelivr.net/npm/animate.css#3.5.1';
</style>
It works very nice and smooth, but... the new coming page enter when the first one is totally gone. This made a gap between transition.
In Vue manual: Transition-Modes there are a few examples. I need to replicate the third button example but I'm missing the mode I have to use.
Any suggestion?
The main problem with your transitioning elements is that you want them to occupy the same space in DOM at the same time (even if, visually, one enters and one exists - that's only done through transforms but the two elements need to occupy the same space in DOM).
Therefore you need to give one of them position:absolute and use CSS to size and position it correctly, to match the exact position and size it would have when not having position:absolute (which is what it will have when not trasitioning).
Here's a working example. Note yours might need different styles applied to a different element.
Since you haven't provided a minimal, reproducible example with your own markup, there's no way to know.
In the example above, I gave the subsequent <div> (the entering one)
position: absolute;
width: 100%;
top: 60px;
left: 0;
If you choose to wrap all your <router-view>s into a common wrapper element with position:relative, top would need to be 0 (in the example 60px is accounting for <nav>'s height).
Note: and yes, as others already pointed, you don't need mode="in-out". But that still leaves you with the positioning issue.
Edit: I've played with two more examples.
one using a flexbox container of height:100vh where top and bottom elements don't grow and middle one does. When middle element is too big, it becomes scrollable.
another one where I played with the transition effects and Bootstrap Vue.
Actually since you don't need any special behaviour and actually want both transitions to happen at the same time, you shouldn't be using the mode at all. Just remove it and it should work as you described. From the docs link you pasted:
Simultaneous entering and leaving transitions aren’t always desirable though, so Vue offers some alternative transition modes
in-out: New element transitions in first, then when complete, the current element transitions out.
out-in: Current element transitions out first, then when complete, the new element transitions in.
mode="in-out": New element transitions in first, then when complete, the current element transitions out.
new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
showOn: true
},
methods: {
handleClick() {
console.log(this.message);
}
}
})
.slide-fade-enter-active {
transition: all .3s ease;
position: absolute;
left: 0;
top: 0;
}
.slide-fade-leave-active {
position: absolute;
left: 0;
top: 0;
}
.slide-fade-enter, .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
#app {
position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<body>
<div id="app">
<transition name="slide-fade" mode="in-out">
<button v-if="showOn"
key="on"
type="button"
#click="showOn=false">On</button>
<button v-else type="button"
key="off"
#click="showOn=true">Off</button>
</transition>
</div>
</body>

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)
}

Scroll bar below fixed header with Vuetify + Electron

I am using Vuetify and Electron to make an app to help me with certain tasks at my job. I have disable the browserWindow frame and made my header the draggable area with a button to close the window. I am using the electron vuetify template
vue init vuetifyjs/electron
My problem is the scrollbar reaches all the way to the top but I would like it below my fixed header.
I have tried playing with overflow properties on the html, body, app div, and content div tags but i have not been successful.
How would I accomplish this?
This is purely a CSS question really as you can see this behaviour in the browser too with similar layouts. The easiest way to fix this is using a flex layout:
HTML:
<div class="container">
<div class="titlebar"></div>
<div class="content">
<h1>So much content we scroll</h1>
<h1>So much content we scroll</h1>
<!-- etc -->
</div>
</div>
CSS:
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.titlebar {
background-color: blue;
height: 35px;
flex-shrink: 0;
}
.content {
flex-grow: 1;
overflow-x: auto;
}
Check out this out in this CodePen
I'd like to offer a Vuetify specific answer for this question, this should apply whether or not Electron is involved.
Vuetify's default styles make this a bit more difficult than a simple CSS solution can give you, especially when the layout gets more complex.
For this example I'm using the complex layout from Vuetify's pre-defined themes here
Vuetify ships with an overflow-y: scroll on the html element so the first step is adding an override for this.
html {
overflow: hidden;
}
This will get rid of the bar on the right side that spans the whole height of the app.
Next you will want to set your v-content area as the scrollable area. There are a few gotchas to watch out for when you're setting this area:
Display flex is already declared
Vuetify sets padding in the style attribute so you'll need to override depending on your case
You'll need a margin the height of your header(only matters if you're changing header height from 64px)
You'll need to remove the header height from the height of the content container using calc(Same as above)
If you have a nav drawer on the right side you'll need to bind a class to take care of this.
My CSS for v-content looks like this, you will need an important to override the padding since it is set by Vuetify through style binding:
main.v-content {
width: 100vw;
height: calc(100vh - 64px);
flex-direction: column;
overflow: scroll;
margin-top: 64px;
padding-top: 0 !important;
}
I also have a class bound to the state of the temporary right drawer on the v-content tag in the template, this makes sure that the scroll bar doesn't disappear underneath the right nav drawer when it's open:
<v-content :class="{ draweropen: drawerRight }">
And the CSS for that bound class, once again you'll need an important to remove the default right padding Vuetify puts on v-content when the drawer is open:
.draweropen {
width: calc(100vw - 300px) !important;
padding-right: 0 !important;
}
You can optionally set the flex-direction to column-reverse if your content is bottom loaded like a chat which is what I'm doing in this CodePen Example
I built a little component that wraps the v-main and moves the scrollbar to the main container instead of the default (the entire html).
Simply replace v-main with this and you're done.
<template>
<v-main class="my-main">
<div class="my-main__scroll-container">
<slot />
</div>
</v-main>
</template>
<script>
export default {
mounted: function() {
let elHtml = document.getElementsByTagName('html')[0]
elHtml.style.overflowY = 'hidden'
},
destroyed: function() {
let elHtml = document.getElementsByTagName('html')[0]
elHtml.style.overflowY = null
},
}
</script>
<style>
.my-main
height: 100vh
.my-main__scroll-container
height: 100%
overflow: auto
</style>

MapBox (mapbox-gl-vue) renders the map on only 50% of the width of the container

I am trying MapBox with Vue 2 and I cannot make the map take the full width of the container. It only renders on 50% of the width of the container.
I have included the files in the head of my index.html as follows:
<script src='https://api.mapbox.com/mapbox-gl-js/v0.40.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v0.40.0/mapbox-gl.css' rel='stylesheet' />
I want the map in a component (Map.vue, I am using vue-router), so here is the code in Map.vue:
Script:
import Mapbox from 'mapbox-gl-vue';
export default {
components: {
'mapbox': Mapbox
}
}
Template:
<mapbox access-token="pk.eyJ1Ijoic3BlZW5pY3Q....."
:map-options="{
style: 'mapbox://styles/mapbox/streets-v9',
center: [-96, 37.8],
zoom: 3
}"
:geolocate-control="{
show: true,
position: 'top-left'
}"
:scale-control="{
show: true,
position: 'top-left'
}"
:fullscreen-control="{
show: true,
position: 'top-left'
}">>
</mapbox>
Style:
#map {
width: 100%;
height: 600px;
position: absolute;
margin:0;
z-index:1;
}
I have tried everything I know in the CSS id but it only renders the map in the right half of the width of the container, in the left one only the logo and the controls are displayed while the rest of the area is empty.
To solve the problem, I just had to delete "text-align: center;" from #app in App.vue.
For more details, check the issue I had opened here:
https://github.com/phegman/vue-mapbox-gl/issues/11
It looks like to me, there is something dynamic with the div or the div is rendered later after the instantiation. I have not used vue, however.
I have had this problem with tabs and div rendered after the page load such as in tabs or triggered by JavaScript.
If you use map.invalidateSize(); where map is the object instantiated. This will redraw the map. Try and put this after the window is loaded to test the code. Then perhaps it can be converted into the correct Vue implementation.
window.addEventListener("load", function(){
map.invalidateSize();
});;