Page not scrolling to top when route changes in Nuxt JS - vue.js

I'm trying to always go on the top of the page when route changes with Nuxt.
So I've put this into my app/router.scrollBehavior.js file:
export default function (to, from, savedPosition) {
return { x: 0, y: 0 }
}
But it always returns to the last saved position (which is always null in my console by the way).
Any idea of what I could miss here?

For Nuxt v3.0.0-rc.3
Create file named route.global.ts on middleware/ folder
Then write this in file:
export default defineNuxtRouteMiddleware((to, from) => {
if (to.path !== from.path && process.client) {
window.scrollTo(0, 0)
}
})

I use Nuxt 3 (npm:nuxt3#3.0.0-rc.4-27588443.cf25525), none of the solutions work for me.
Finally this works:
/plugins/scrollToTop.js (any filename will work, just put it in the plugins folder)
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.$router.options.scrollBehavior = async (to, from, savedPosition) => {
if (to.path !== from.path && process.client) {
window.scrollTo(0, 0);
}
};
});

According to the documentation:
https://nuxtjs.org/docs/configuration-glossary/configuration-router#scrollbehavior
The router.scrollBehavior.js file must be in the app folder, which in turn is in the project's root.
the file should name router.scrollBehavior.js.
You can console.log something in this function and check if it works.

At the end, GSAP was conflicting with OP's scrolling behavior.
Removing it, solved all the issues related to Nuxt, nothing was actually wrong with the router.scrollBehavior.js file.

The top answer is not the correct way of doing it.
Create a file in app directory called router.options.js
// app/router.options.js
export default {
scrollBehavior() {
return { top: 0 }
}
}
With typescript (recommended)
// app/router.options.ts
import type { RouterOptions } from '#nuxt/schema'
export default <RouterOptions> {
scrollBehavior() {
return { top: 0 }
}
}

For Nuxt 3
My solution was to create a file in the middleware folder with the following stucture:
export default defineNuxtRouteMiddleware((to, from) => {
useNuxtApp().hook("page:finish", () => {
if (history.state.scroll) {
setTimeout(() => window.scrollTo(history.state.scroll), 0);
} else {
setTimeout(() => window.scrollTo(0, 0), 0);
}
});
})
and I named it fix-scroll-position.global.ts.
The setTimeout is used to avoid the weird jumping to the top, meaning that the user won't see the page scrolling to the top.
This snippet ensures that the page scrolls to the top when the routes change, and that the scroll position is kept when the back button is clicked. The last functionality is achieved using history.state.scroll, that checks if there is a scroll position saved from the previous route.

I don't have enough reputation for a comment, so therefore an answer. A simple note, but I think it may help some.
I struggled with this as well and nothing worked, but after a while I found out the culprit. My layout looked like this
<template>
<div id="page" class="flex flex-col h-screen overflow-scroll bg-white">
<Navbar />
<slot />
<Footer />
</div>
</template>
That div around the components has the height of the screen and the page content scrolls in that div, but therefore the window is always at the top and you won't see any scrolling when clicking on NuxtLink.
I use nuxt 3.0.0 and when I let the #page div grow with the content (ie. removing the h-screen and overflow-scroll tailwind classes), it has the scroll to top behavior even without the solutions from above.
So, if you have this problem with the stable nuxt 3 version, check your html.

Related

How to create dynamic components in vueJS

I am new to vueJS and am trying to load components dynamically. I searched on the web and experimented with many suggestions but am still not able to get it to work.
Scenario: I want to have a 'shell' component that acts as a container for swapping in and out other components based on user's selection. The file names of these components will be served up from a database
Here's the shell component:
<template>
<keep-alive>
<component :is='compName'></component>
</keep-alive>
</template>
<script>
props: ['vueFile'],
export default ({
data() {
compName: ()=> import('DefaultPage.vue')
},
watch: {
compName() {
return ()=> import(this.vueFile);
}
}
})
</script>
This does not work.
If I hard code the component file name, it works correctly... e.g.
return ()=> import('MyComponent.vue');
The moment I change the import statement to use a variable, it gives an error even though the variable contains the same string I hard code.
What am I doing wrong?
compName() {
const MyComponent = () => import("~/components/MyComponent.js");
}
You can see this post
https://vuedose.tips/dynamic-imports-in-vue-js-for-better-performance
You can put the components you want to add dynamically in a directory, for example: ./component, and try this
compName () {
return ()=> import(`./component/${this.vueFile}`);
}
The import() must contain at least some information about where the module is located.
https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import

Problem to add a root property to a vue app

Could someone tell me what is wrong with this setup, where I want to load a config.json file before the vue app is created and access the config in the components with this.$root.config. There is no root config element I can access? Missing something? Thanks for your help! The config.json file is correctly loaded, can log the config to the console. But it is not added to the root properties from Vue?
fetch('/config.json')
.then(res => res.json())
.then(config => {
createApp(App, {
data() {
return config
},
created() {
console.log(this.$root.config);
}
}).use(store).use(router).use(i18n).mount('#app');
});
What you place in data won't be found in $root but, as Abdelillah pointed out, in $root.$data. Since App is the root component, though, you can just use this.config. In any subcomponent, you'd have to use this.$root.$data.config.
But Vue 3 provides a cleaner alternative to provide data to any component in your app: config.globalProperties.
Example:
const app = Vue.createApp({});
app.component('test', {
mounted() {
console.log(this.config);
}
});
Promise.resolve({
foo: 'bar'
}).then(config => {
app.config.globalProperties.config = config;
app.mount('#app');
});
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
<div id="app">
<test />
</div>
As you can see, logging <test>'s .config outputs the globalProperty.config set on the app, and it's going to be the same for any component in the app.
If you want to provide data to any descendants of current component (no matter how deep, skipping intermediary parents), you could use provide/inject. While I find this particularly useful for providing some data to all the children of a particular component (and not to the rest of the app's components), it can obviously be used on the root component, which would make the provide available cross-app via inject wherever you need it.
there is no property called config on your data, what you are doing is simply returning the JSON object you imported, you should be doing:
fetch('/config.json')
.then(res => res.json())
.then(config => {
createApp(App, {
data() {
return {
// config: config
config
}
},
created() {
console.log(this.$root.$data.config);
// or console.log(this.config);
}
}).use(store).use(router).use(i18n).mount('#app');
});

How do I display the captcha icon only on certain pages (VUE reCAPTCHA-v3)?

I use this package : https://www.npmjs.com/package/vue-recaptcha-v3
I add on my main.js :
import { VueReCaptcha } from 'vue-recaptcha-v3'
Vue.use(VueReCaptcha, { siteKey: 'xxxxxxx' })
I add this code :
await this.$recaptcha('login').then((token) => {
recaptcha = token
})
to my component to get token from google recapchta
My problem is the captcha icon in the lower right corner appears on all pages
I want it to only appear in certain components
Maybe I must to change this : Vue.use(VueReCaptcha, { siteKey: 'xxxxxxxxxxxxxxxxx' }). Seems it still mounting to Vue.use. I want to mount to a certain component instead of vue root instance
How can I solve this problem?
Update
I try like this :
Vue.use(VueReCaptcha, {
siteKey: 'xxxxxxx',
loaderOptions: {
useRecaptchaNet: true,
autoHideBadge: true
}
})
It hides the badge. I want the badge to still appear. But only on 1 page, the registration page. How can I do it?
I've had the same issue while using the npm package, it's pretty annoying.
At the end of the day, I've decided not to use the package & follow Google's documentation.
This line here :
grecaptcha.execute('_reCAPTCHA_site_key_', {action: 'login'}).then(function(token) {
recaptcha = token
})
Is equivalent to this line here from the npm package :
this.$recaptcha('login').then((token) => {
recaptcha = token
})
You just need to add this line into your < head > for recaptcha to work :
<script src="https://www.google.com/recaptcha/api.js?render=_reCAPTCHA_site_key"></script>
But as soon the script tag is in your < head >, you will be facing the same issue of it showing on every page.
The hack is that you only insert it into the < head > on components that you need.
There are ways to do this but I ended up referencing this.
You can put it in the methods of your component & call the method when the component is loaded.
That way it will only show up on the pages that you need it to.
in main.js set autoHideBadge true:
import { VueReCaptcha } from 'vue-recaptcha-v3'
Vue.use(VueReCaptcha, { siteKey: 'your site key',
loaderOptions:{autoHideBadge: true }})
in every page you want to show the badge you can show the badge in mounted,
for some reasons until a few seconds after mounted event this.$recaptchaInstance is null and you cant use it, so I use a timeout to showing the badge 5 second after page load in mounted.
mounted(){
setTimeout(()=>{
const recaptcha = this.$recaptchaInstance
recaptcha.showBadge()
},5000)
},
when you show it you have to hide it again in the same page.
beforeDestroy() {
const recaptcha = this.$recaptchaInstance
recaptcha.hideBadge()
},
If you are using composition API setup this is what you need:
const reCaptchaIn = useReCaptcha().instance
onMounted(() => {
setTimeout(() => {
reCaptchaIn.value.showBadge()
}, 3000)
})
Just use this code:
const recaptcha = this.$recaptchaInstance
// Hide reCAPTCHA badge:
recaptcha.value.hideBadge()
// Show reCAPTCHA badge:
recaptcha.value.showBadge()
vue-recaptcha-v3 npm
I stumbled upon this incredibly simple answer. It is excellent especially if you wish to hide the badge from all your pages. You can perhaps use scoped css to hide on some pages as well.
.grecaptcha-badge { visibility: hidden; }
You can read the post here

Page reload causes Vuex getter to return undefined

Using Vue.js (Vuetify for FE).
A page reload causes the getter in Vuex to fail with pulling required data from the store. The getter returns undefined. The code can be found on GitHub at: https://github.com/tineich/timmyskittys/tree/master/src
Please see the full details on this issue at timmyskittys.netlify.com/stage1. This page has complete info on the issue and instructions on how to view the issue.
Note, there is mention of www.timmyskittys.com in the issue description. This is the main site. timmyskittys.netlify.com is my test site. So, they are the same for all intents and purposes. But, my demo of this issue is at the Netlify site.
I read the complete issue in the website you mentioned. It's a generic case.
Say, for cat details page url: www.timmyskittys.com/stage2/:id.
Now in Per-Route Guard beforeEnter() you can set the cat-id in store. Then from your component call the api using the cat-id (read from getters)
I found the solution to my issue:
I had to move the call of the action which calls the mutation that loads the .json file (dbdata.json) into a computed() within App.vue. This was originally done in Stage1.vue.
Thanks all for responding.
I had the same issue and my "fix" if it can be called that was to make a timer, so to give the store time to get things right, like so:
<v-treeview
:items="items"
:load-children="setChildren"
/>
</template>
<script>
import { mapGetters } from 'vuex'
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
export default {
data () {
return {
children: []
}
},
computed: {
...mapGetters('app', ['services']),
items () {
return [{
id: 0,
name: 'Services',
children: this.children
}]
}
},
methods: {
async setChildren () {
await pause(1000)
this.children.push(...this.services)
}
}
}
</script>
Even though this is far from ideal, it works.

Nuxt - login without redirect

I'm trying to create authentication on my Nuxt app, but every tutorial I've found is using redirect to public / private paths.
Example:
if (user.isLoggedIn()) {
redirect('/dashboard')
} else {
redirect('/login')
}
I'm used to react way, where I have a single wrapper component in which I decide by the state if I want to show public (login) or private (dashboard) page.
Example of index page (route path '/'):
export default = ({ viewer }) =>
viewer.isLoggedIn ? <Dashboard /> : <Login />
Is there any way to achieve this in Nuxt?
You have to set a dynamic layout parameter in your Page.vue files.
first step, set dynamic layout in your Page.vue:
export default {
layout (context) {
return context.isLoggedIn ? 'privateLayout' : 'publicLayout';
}
}
second step, set a Context custom var (or better, in your Store) from a middleware auth:
export default function (context) {
context.isLoggedIn = true; //or false, insert your auth checking here
}
see documentation: https://nuxtjs.org/api/pages-layout#the-layout-property
see live example: https://glitch.com/edit/#!/nuxt-dynamic-layouts?path=pages/index.vue:10:8
You can use your index page as a wrapper for two components that show depending on whether the user is logged in. So inside your index.vue:
<template>
<div class="wrapper">
<dashboard v-if="userIsLoggedIn" />
<login v-else />
</div>
</template>
Then you could write the dashboard and login component as separate pages and even switch dynamically between them by making userIsloggedIn reactive.
Hope that's more like what you're looking for.