How to stop FOUC when leaving route with 'vue-flickity' carousel/slider? - vue.js

I'm using the vue-flickity package for MetaFizzy's Flickity in my Vue app. When navigating away from a route that has an instance of vue-flickity slider, you get a brief FOUC showing all the slides unstyled in the document as the <Flickity /> slider is dismounted(?) and the view changes.
I've tried wrapping it in a <keep-alive>, but that doesn't help.
What would be the best approach to hiding or "animating out" the component before the user navigates away from a route?
I also tried to use beforeRouteLeave(), transition opacity on <Flickity ref="mySlider" /> to 0, then change route. I tried something like the following, but it didn't work as expected:
// SliderView.vue
<template>
<Flickity ref="mySlider">
<div v-for="(slide, index) in slides" :key="index">
// slide content markup
</div>
</Flickity>
</template>
<script>
import 'Flickity' from 'vue-flickity'
export default {
name: 'SliderView'
}
</script>
// router.js
import Vue from 'vue'
import Router from 'vue-router'
import SliderView from './views/SliderView.vue'
export default new Router({
routes: [
{
path: '/routeWithSlider',
component: SliderView,
beforeRouteLeave (to, from, next) {
const slider = this.$refs.mySlider
if (slider) {
slider.style.transition = 'opacity .5s'
slider.style.opacity = 0
setTimeout(() => {
next()
}, 600)
} else {
next()
}
}
}
]
})
Is this the correct approach, or should I be approaching this differently?
Also, if there was a way to do this globally for all <Flickity /> instances on all routes without specifying refs, that would also be useful too.
I haven't been using Vue for that long, so am looking for some guidance on the direction to take.

I found the best way to acheive this is to use BeforeRouteLeave() lifecycle hook on the component with the slider to animate the Flickity slider exit before changing route:
beforeRouteLeave (from, to, next) {
const slider = this.$refs.flickity.$el
slider.style.transition = 'opacity .25s ease'
slider.style.opacity = 0
setTimeout(() => {
next()
}, 250)
}

Related

Vue route with dynamic import causes default layout to flash for split of second

I'm using Vue 3 with vue-router and vite-plugin-pages, which by default loads all route components asynchronously.
I have made a simple layout component which switches between LayoutDefault and LayoutBlank, based on the route's meta. Blank is only for public sites like the login page, where user is not yet logged in. I made it this way so I only need to set the layout on few routes instead of all of them.
Because of the dynamic import though, when entering the site and being redirected to the login site via router's beforeEach guard, you can see the default layout being drawn for a split of a second and then switches to blank layout. This happens, because route.meta.layout in the watcher is always undefined at the beginning.
I could make /login to load synchronously, but I don't like this approach as I might be adding more "public" routes that should render in LayoutBlank, and same glitch also happens on them even when entering directly and not being redirected by the router.
Is there any fix to this other than switching the order of layouts so blank is default or loading everything synchronously? I tried to use another beforeEach hook instead of watching route.meta.layout but all it did, was moving the glitch to route leaving instead of entering when layouts were switched.
I couldn't use Vue3 in snippet so I put a simple demo code here:
https://stackblitz.com/edit/vue3-vue-router-meta-layout-spc5tq?file=src%2Flayouts%2FAppLayout.vue,src%2Frouter.ts
When you reload the web container, you will notice the red "Default Layout" text flicker.
Some code:
<script setup lang="ts">
import AppLayoutDefault from './AppLayoutDefault.vue'
import AppLayoutBlank from './AppLayoutBlank.vue'
import { markRaw, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const layout = ref()
const route = useRoute()
watch(
() => route.meta?.layout as string | undefined,
(metaLayout) => {
layout.value = markRaw(metaLayout === 'blank' ? AppLayoutBlank : AppLayoutDefault)
},
{ immediate: true }
)
</script>
<template>
<component :is="layout"> <router-view /> </component>
</template>
import { createRouter, createWebHistory } from 'vue-router'
import Home from '#/views/Home.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Home,
},
{
path: '/login',
component: () => import('#/views/Login.vue'),
meta: { layout: 'blank', public: true },
},
{
path: '/admin',
component: () => import('#/views/Admin.vue'),
meta: { layout: 'AppLayoutAdmin' },
},
],
})
let isLoggedIn = false
router.beforeEach((to) => {
if (!to.meta.public && !isLoggedIn) {
isLoggedIn = true
return 'login'
}
})
export default router
I have actually found an answer! It was caused by most examples on the internet not ever mentioning router.isReady()! I was curious why route.fullPath returned / on every page load, even when entering /login. This led me to this snippet:
router.isReady().then(() => app.mount())
And guess what, delaying app.mount() like that actually fixed my whole problem. Now the first layout that is rendered is actually the one configured in the route, even if it's not the default one.

How to use Vue Router scroll behavior with page transition

I'm using Vue Router 4 on an application where the top level RouterView is wrapped in a transition, like so:
<router-view v-slot="{ Component }">
<transition mode="out-in">
<component :is="Component" />
</PageTransition>
</router-view>
When I try to add scroll behavior to the Router, to scroll to a specific element when users navigate back to the index page, the scroll behavior fires during the out phase of the transition, when the index page isn't mounted yet, so the element isn't found.
eg.
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior (to, from) {
if (to.name === 'index' && isContentPage(from)) {
return { el: '#menu' }
}
return undefined
}
})
I would get a warning in the console: Couldn't find element using selector "#menu" returned by scrollBehavior.
The Vue Router docs on scroll behavior mention that it's possible to work around this issue by hooking up to transition events:
It's possible to hook this up with events from a page-level transition component to make the scroll behavior play nicely with your page transitions, but due to the possible variance and complexity in use cases, we simply provide this primitive to enable specific userland implementations.
But I couldn't figure out what sort of approach it was suggesting, nor could I find any "userland implementations".
Finally I found a solution to this, which was to use a module holding some state—in this case a Promise—to act as the link between the transition and the router.
The module:
let transitionState: Promise<void> = Promise.resolve()
let resolveTransition: (() => void)|null = null
/**
* Call this before the leave phase of the transition
*/
export function transitionOut (): void {
transitionState = new Promise(resolve => {
resolveTransition = resolve
})
}
/**
* Call this in the enter phase of the transition
*/
export function transitionIn (): void {
if (resolveTransition != null) {
resolveTransition()
resolveTransition = null
}
}
/**
* Await this in scrollBehavior
*/
export function pageTransition (): Promise<void> {
return transitionState
}
I hooked up the transition events:
<router-view v-slot="{ Component }">
<transition mode="out-in" #before-leave="transitionOut" #enter="transitionIn">
<component :is="Component" />
</PageTransition>
</router-view>
...and in the Router:
const router = createRouter({
history: createWebHashHistory(),
routes,
async scrollBehavior (to, from) {
if (to.name === 'index' && isContentPage(from)) {
await pageTransition()
return { el: '#menu' }
}
return undefined
}
})
What's more, I actually have a nested RouterView also wrapped in a transition. With that transition extracted to a component, both instances could call transitionOut and transitionIn and it seems to work (though I haven't tested it much for race conditions).
If anyone has found simpler solutions though, I'd be interested to see them.

How to prevent double invoking of the hook created?

So I have a problem with my vue3 project. The gist: I need to support different layouts for some use cases: authorization, user profile's layout, group's layout, etc. I've got the opportunity by this way:
Create a component AppLayout.vue for managing layouts
<template>
<component :is="layout">
<slot />
</component>
</template>
<script>
import AppLayoutDefault from "#/layouts/EmptyLayout";
import { shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
export default {
name: "AppLayout",
setup() {
const layout = shallowRef(AppLayoutDefault);
const route = useRoute();
watch(
() => route.meta,
async (meta) => {
try {
const component =
await require(`#/layouts/${meta.layout}.vue`);
layout.value = component?.default || AppLayoutDefault;
} catch (e) {
layout.value = AppLayoutDefault;
}
}
);
return { layout };
},
};
</script>
So my App.vue started to look so
<template>
<AppLayout>
<router-view />
</AppLayout>
</template>
To render a specific layout, I've added to router's index.js special tag meta
{
path: '/login',
name: 'LoginPage',
component: () => import('../views/auth/LoginPage.vue')
},
{
path: '/me',
name: 'MePage',
component: () => import('../views/user/MePage.vue'),
meta: {
layout: 'ProfileLayout'
},
},
Now I can create some layouts. For example, I've made ProfileLayout.vue with some nested components: Header and Footer. I use slots to render dynamic page content.
<template>
<div>
<div class="container">
<Header />
<slot />
<Footer />
</div>
</div>
</template>
So, when I type the URL http://example.com/profile, I see the content of Profile page based on ProfileLayout. And here the problem is: Profile page invokes hooks twice.
I put console.log() into created() hook and I see the following
That's problem because I have some requests inside of hooks, and they execute twice too. I'm a newbie in vuejs and I don't understand deeply how vue renders components. I suggest that someting inside of the code invokes re-rendering and Profile Page creates again. How to prevent it?
Your profile page loaded twice because it's literally... have to load twice.
This is the render flow, not accurate but for you to get the idea:
Your layout.value=AppDefaultLayout. The dynamic component <component :is="layout"> will render it first since meta.layout is undefined on initial. ProfilePage was also rendered at this point.
meta.layout now had value & watcher made the change to layout.value => <component :is="layout"> re-render 2nd times, also for ProfilePage
So to resolve this problem I simply remove the default value, the dynamic component is no longer need to render default layout. If it has no value so it should not render anything.
<keep-alive>
<component :is="layout">
<slot />
</component>
</keep-alive>
import { markRaw, shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
export default {
name: "AppLayout",
setup() {
console.debug("Loaded DynamicLayout");
const layout = shallowRef()
const route = useRoute()
const getLayout = async (lyt) => {
const c = await import(`#/components/${lyt}.vue`);
return c.default;
};
watch(
() => route.meta,
async (meta) => {
console.log('...', meta.layout);
try {
layout.value = markRaw(await getLayout(meta.layout));
} catch (e) {
console.warn('%c Use AppLayoutDefault instead.\n', 'color: darkturquoise', e);
layout.value = markRaw(await getLayout('EmptyLayout'));
}
}
);
return { layout }
},
};

Keeping State of Vue

Is there a way to keep a state of a vue so that when navigated back it is shown?
Example:
1) I'm on Page A, search something, items are loaded, I scroll down and pick item 34 from the list.
2) Now Page B opens with information about the item. I click back but end on an empty Page A.
What I would want is the search results and ideally the scroll position from when I left that vue.
Is that possible out of the box in vue2 or do I have to code all of that myself?
So you want to somehow vuejs to remember to scrollPosition.As always that is very simple with vue.
The scroll behavior can managed by Vue-Router.I'll show you an example.
The Page component with 2 scroll positions
<template>
<div>
<div id="data1" style="height:700px;background:blue">
First scroll position
</div>
<div id="data2" style="height:700px;background:yellow">
Second scroll position
</div>
</div>
</template>
The component which navigate to page component
<template>
<div>
<router-link
tag="button"
:to="link"
>
Navigate to Page in scroll Position 2
</router-link>
</div>
</template>
<script>
export default {
data: () => ({
link: {
name: 'Page',
hash: '#data2' //<= important,this adds the #data2 in url
}
})
}
</script>
Now your route file have to look like this:
import Vue from 'vue'
import Router from 'vue-router'
import Page from '#/components/Page.vue'
import NavigationButton from '#/components/NavigationButton.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/Page',
name: 'Page',
component: Page,
},
{
path: '/',
name: 'NavigationButton',
component: NavigationButton
},
//other routes
],
mode: 'history',
scrollBehavior(to, from, savedPosition) {
if(savedPosition){ //when use press back button will go at the position he was on the page
return savedPosition
}
if(to.hash){ //if has a hash positition to go
return { selector: to.hash } //go to the page in scrolled Position
}
return { x:0, y: 0 } //go to the page in scroll = 0 Position
}
})
That i understood by your question,but i am not sure your asking about that.Anyway if i am right,dont forget also to take a look to official vue-router documentation about scroll behavior.See here

vuejs route between components scroll same

<template>
<div class="index">
<common-header id="common-header" class="common-header" v-el:commonheader></common-header>
<router-view transition keep-alive class="index-view"></router-view>
</div>
</template>
the route view will show two component A and B, while component A scrollTop is 0, I route to component B, and scroll down, and then route to component A, A is also scroll. Anyone have any ideas?
You can add a global before hook to the router, which will be called before every route transition starts and scroll to the top of the page. That is how I have solved it. Read here
Vue 1.
router.beforeEach(function (transition) {
window.scrollTo(0, 0)
transition.next()
})
For Vue2:
const router = new VueRouter({
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
Refer here
For Vuejs2.0, there is a new accepted way dealing with scroll behavior on page changes:
import VueRouter from 'vue-router';
const router = new VueRouter({
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
This will scroll to the top of the page after each navigation change. You can read up on the full, official documentation about this functionality here.
I think you are scroll the wrong element.I have make the same mistake like that:component A and component B is both in element Body,and I make the body scroll,so as long as you move the scroll bar,it will work for both A&B.I finally work out it by scrolling component instead of scrolling the body.