Scrollbehaviour weird jumping in Nuxt 3 - vue.js

So I configured my rouse.scrollBehaviour.ts file in Nuxt 3. Here it is:
import { defineNuxtPlugin } from "#app";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.$router.options.scrollBehavior = (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return { left: 0, top: 0, behaviour: "smooth" };
};
});
The problem is that I think it's scrolling earlier than the pages load, so there is a weird jumping effect on the pages.
Here is a video of the problem:
https://user-images.githubusercontent.com/22452368/154849559-3974fc01-e265-486b-865b-55ee03053fa8.mp4
Can you please help me what is the problem here? Or is there a bug in Nuxt 3?

You can try this solution mentioned in this nuxt issue
// app/router.options.js
export default {
scrollBehavior() {
return { top: 0 }
},
}

Related

NuxtLink to hash

I'm trying to use a NuxtLink to scroll to an anchor tag. From the docs I see I need to create this file app/router.scrollBehavior.js and place my code there.
For example, this works. It scrolls 500px in the y axis, but what I really want is to scroll to the hash.
export default function (to, from, savedPosition) {
if (to.hash) {
return { x: 0, y: 500 }
}
}
Events Page
<div
v-for="(event, i) in events"
:id="event.id"
:ref="event.id"
:key="i"
>
</div>
Navigation Component
<NuxtLink
v-for="item in items"
:key="`item.id"
:to="item.href"
>
{{ item.name }}
</NuxtLink>
I haven't been able to make it scroll to the hash. I tried several options an none of them seems to work. For example:
Does not work (I also tested with selector instead of el)
export default function (to, from, savedPosition) {
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth',
}
}
}
Does not work
export default function (to, from, savedPosition) {
return new Promise((resolve, reject) => {
if (to.hash) {
setTimeout(() => {
resolve({
el: to.hash,
behavior: 'smooth',
})
}, 500)
}
})
}
Any ideas about what it could be the problem?
Ok, so finally this is what worked for me. I had to install Vue-Scroll and inside app/router.scrollBehavior.js
import Vue from 'vue'
import VueScrollTo from 'vue-scrollto'
Vue.use(VueScrollTo)
export default function (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (to.hash) {
VueScrollTo.scrollTo(to.hash, 300, { easing: 'linear', offset: -60 })
}
}, 500)
})
}
I used this answer as a reference How to smoothly scroll to an element via a hash in Nuxt? but I had to change the setTimeOut time from 10ms to 500ms because it wasn't working properly on my static page.

Anchor in Nuxt component not working on same page the anchor is included on

In my Footer Component I have this to link to the owners bio on the about page
<nuxt-link :to="{path: '/about', hash: 'alex'}">Alex</nuxt-link>
In the about/index.vue file I have the anchor
<hr class="my-5" id="alex" />
<h2 style>
Alex
<br />
<span style="font-size: 16px; font-weight: bold;">Co-Founder and Partner</span>
</h2>
On all pages this works when you click the link in the footer. It does not work if you are on the about page and click the footer link.
What can I do to make this also work on the about page?
Update Nuxt Link as below
<nuxt-link :to="{path: '/about', hash: '#alex'}">Alex</nuxt-link>
++ Updated
Need to add scroll behavior in nuxt.config.js as below
router: {
scrollBehavior: async function(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
}
const findEl = async (hash, x = 0) => {
return (
document.querySelector(hash) ||
new Promise(resolve => {
if (x > 50) {
return resolve(document.querySelector("#app"));
}
setTimeout(() => {
resolve(findEl(hash, ++x || 1));
}, 100);
})
);
};
if (to.hash) {
let el = await findEl(to.hash);
if ("scrollBehavior" in document.documentElement.style) {
return window.scrollTo({ top: el.offsetTop, behavior: "smooth" });
} else {
return window.scrollTo(0, el.offsetTop);
}
}
return { x: 0, y: 0 };
}
},
Codesandbox Link
You can use vue-scrollto package also and if you are using Vuetify with Nuxtjs than there is $vuetify.goTo available.
Just wanted to add, if people are stuck, you can add a file called router.scrollBehavior.js in your nuxt project:
https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-router#scrollbehavior
Unfortunately, there are issues with it firing before the render - you can use nuxt/vue tick if you correctly import - but this still seems to work ( for anchors and for saved positions ):
export default async function (to, from, savedPosition) {
if (savedPosition) {
console.log("SAVED POSITION");
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
selector: savedPosition
});
}, 600);
});
// not working consistently due to render
//return savedPosition;
}
else if (to.hash) {
console.log("HASH");
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
selector: to.hash
});
}, 600);
});
}
else {
console.log("NORMAL");
return { x: 0, y: 0 };
}}
I am using Nuxt 3 and having the same problem, I solved the problem using this with thoughts of helping Nuxt 3 devs with same problem
Anchor tag with NuxtLink
<NuxtLink :to="{path: '/about', hash: '#support'}">Support</NuxtLink>
On the Nuxt page <script setup>
// Make sure you are in the client
// browser to access `document` variable
if (process.client) {
// get current route
const {currentRoute : route} = useRouter();
// make sure that hash is defined
if (route.value?.hash) {
// set hash
const hash = route.value?.hash
// use onMounted hook to run the code inside
// after the page is mounted to the DOM
onMounted(() => {
// get target element
let el = document.querySelector(hash)
// make sure that the element exists then scroll to that element
if(el) {
if ('scrollBehavior' in document.documentElement.style) {
window.scrollTo({ top: el.getBoundingClientRect().top+window.scrollY, behavior: 'smooth' })
} else {
window.scrollTo(0, el.getBoundingClientRect().top+window.scrollY)
}
}
})
}
}
If your are on Vue 3, you will have to create a file here app/router.options.ts with the following code inside
import type { RouterConfig } from '#nuxt/schema'
// https://router.vuejs.org/api/#routeroptions
export default <RouterConfig>{
scrollBehavior(to, from, savedPosition) {
return { el: to.hash }
}
}
Your nuxt link should look like this
<NuxtLink :to="{ path: '/', hash: '#projects' }">Projects</NuxtLink>
Checkout more more

Vue content modified after serverPrefetch on client side, when using SSR

I am working with Vue, by means of Quasar, with the pages being rendered via SSR. This works well enough, but I have a component that doesn't seem to behaving properly.
The issue is that the content is rendered correctly on the server side (verified by checking network log in Chrome), with the axios call loading in the data into an element using v-html, but when we get to the browser the state seems to be reset and server side rendered content gets lost, when using the 'elements' tab in the inspector.
Any ideas?
The Vue component is as follows:
<template>
<div class="dy-svg" v-html="svgData"></div>
</template>
<script>
/**
* This provides a way of loading an SVG and embedding it straight into
* the page, so that it can have css applied to it. Note, since we are
* using XHR to load the SVG, any non-local resource will have to deal
* with CORS.
*/
import axios from 'axios';
export default {
props: {
src: String,
prefetch: {
type: Boolean,
default: true
}
},
data() {
return {
svgData: undefined,
};
},
async serverPrefetch() {
if (this.prefetch) {
await this.loadImage();
}
},
async mounted() {
// if (!this.svgData) {
// await this.loadImage();
// }
},
methods: {
async loadImage() {
try {
let url = this.src;
if (url && url.startsWith('/')) {
url = this.$appConfig.baseUrl + url;
}
const response = await axios.get(url);
let data = response.data;
const idx = data.indexOf('<svg');
if (idx > -1) {
data = data.substring(idx, data.length);
}
this.svgData = data;
} catch (error) {
console.error(error);
}
}
}
};
</script>
Note, I did try add the v-once attribute to the div, but it seems to have no impact.
Environment:
Quasar 1.1.0
#quasar/cli 1.0.0
#quasar/app 1.0.6
NodeJS 10.15.3
Vue 2.6.10 (dependency via Quasar)
The fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, you should pre-fetch and fill data into the store while rendering. For this you can use Vuex.
Example Vuex store file:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
// import example from './module-example'
Vue.use(Vuex)
export default function ( /* { ssrContext } */ ) {
const Store = new Vuex.Store({
state: () => ({
entities: {}
}),
actions: {
async get({
commit
}) {
await axios.get('https://example.com/api/items')
.then((res) => {
if (res.status === 200) {
commit('set', res.data.data)
}
})
}
},
mutations: {
set(state, entities) {
state.entities = entities
},
},
modules: {},
// enable strict mode (adds overhead!)
// for dev mode only
strict: process.env.DEV
})
return Store
}
Example Vue page script:
export default {
name: 'PageIndex',
computed: {
// display the item from store state.
entities: {
get() {
return this.$store.state.entities
}
}
},
serverPrefetch() {
return this.fetchItem()
},
mounted() {
if (!this.entities) {
this.fetchItem()
}
},
methods: {
fetchItem() {
return this.$store.dispatch('get')
}
}
}
This should solve the issue you're facing.

Delay the main transition before my custom beforeEnter transition whilst switching routes?

I have this code which does a nice transition effect. Forgive its ugliness but it does the job wonderfully.
mixin:
export default {
transition: {
name: "js-swipe-out",
mode: "out-in",
beforeEnter(el) {
var loader = document.querySelector(".o-loader");
var wrap = document.querySelector(".o-wrapper");
setTimeout(() => {
loader.classList.add("is-loading");
setTimeout(() => {
wrap.classList.remove("is-active");
setTimeout(() => {
loader.classList.add("is-loaded");
setTimeout(() => {
loader.classList.remove("is-loaded");
loader.classList.remove("is-loading");
setTimeout(() => {
// anim in new page elements
wrap.classList.add("is-active");
}, 0);
}, 1099);
}, 500);
}, 1099);
}, 0);
}
}
};
CSS
.js-swipe-out-enter-active,
.js-swipe-out-leave-active {
transition: 1.2s all ease;
}
.js-swipe-out-enter,
.js-swipe-out-leave-active {
opacity: 0;
transform: translateY(-33.333vh);
}
The problem is that js-swipe-out happens at the same time so my animation doesn't work as I would like. For example, you can't always see 100% of the custom transition I have, especially when the menu is open for example.
A Github article said I can use middleware to delay it or pause it, but this delays the whole thing, not just js-swipe-out as I am after.
export default ({ isServer }) => {
if (isServer) return
// Return a promise to tell nuxt.js to wait for the end of it
return new Promise((resolve) => {
setTimeout(resolve, 1000);
})
}
Any ideas of how I can tell Nuxt to do beforeEnter(el) first and then do js-swipe-out after? Once my custom code in beforeEnter has completed?

Vue.js scroll to top of new page route after setTimeout

I have a page transition that doesn't work nicely when the scroll to the top of a new route is instant. I'd like to wait 100ms before it automatically scrolls to the top. The following code doesn't end up scrolling at all. Is there a way to do this?
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home
}
],
scrollBehavior (to, from, savedPosition) {
setTimeout(() => {
return { x: 0, y: 0 }
}, 100);
}
})
This is natively supported by Vue now, use scrollBehaviour, like this:
export default new Router({
scrollBehavior() {
return { x: 0, y: 0 };
},
routes: [
{
path: '/',
name: 'Home',
component: Home
}
],
mode: 'history'
});
More here.
The other answers fail to handle edge cases such as:
Saved
Position - The saved position occurs when the user clicks the back or forward positions. We want to maintain the location the user was looking at.
Hash Links - E.g. http://example.com/foo#bar should navigate to the element on the page with an id of bar.
Finally, in all other cases we can navigate to the top of the page.
Here is the sample code that handles all of the above:
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
scrollBehavior: (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition;
} else if (to.hash) {
return {
selector: to.hash
};
} else {
return { x: 0, y: 0 };
}
}
});
If you want this to happen on every route, you can do so in the before hook in the router:
const router = new VueRouter({ ... })
router.beforeEach(function (to, from, next) {
setTimeout(() => {
window.scrollTo(0, 0);
}, 100);
next();
});
If you are on an older version of vue-router, use:
router.beforeEach(function (transition) {
setTimeout(() => {
window.scrollTo(0, 0);
}, 100);
transition.next();
});
If you want to wait a long time use Async Scrolling of scrollBehaviour, like this:
export default new Router({
scrollBehavior() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 100)
})
},
routes: [
{
path: '/',
name: 'Home',
component: Home
}
],
mode: 'history'
});
More here.
This is probably not the best way, but adding
document.body.scrollTop = document.documentElement.scrollTop = 0;
in a route's core component's (in this case, Home) mounted() function achieves what I want.
When using client-side routing, we may want to scroll to top when navigating to a new route, or preserve the scrolling position of history entries just like real page reload does. vue-router allows you to achieve these and even better, allows you to completely customize the scroll behavior on route navigation.
Note: this feature only works if the browser supports history.pushState.
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
With Saved Position:
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
For more information