Vue router - path #ID scrollBehavior - vue.js

I have created a MENU where I link via <router-link> but certain links are linked to the same page using (anchors).
When I'm on the Work page and I click on the #services section, which is on the Bio page, the section is displayed correctly, but if I want to go to the services section on the Bio page, the URL just changes, but it won't go to the right section for me.
noubtest.com
NAVIGATION
<router-link v-show="!mobile" class="link bio" :to="{ name: 'Home' }">Bio</router-link>
<router-link v-show="!mobile" class="link link2" :to="{ name: 'Services' }">Services</router-link>
<router-link v-show="!mobile" class="link link2" :to="{ name: 'SelectedWork' }">Work</router-link>
ROUTER
{
path: "/home",
name: "Home",
component: Home,
meta: {
title: "Bio",
requiresAuth: false,
},
},
{
path: "/home#fifthPage",
name: "Services",
component: Home,
meta: {
title: "Services",
requiresAuth: false,
},
},
const router = new VueRouter({
mode: "history",
routes,
scrollBehavior() {
return { x: 0, y: 0 };
},
});
router.beforeEach((to, from, next) => {
document.title = `${to.meta.title} | YounesFilm`;
next();
});
router.beforeEach(async (to, from, next) => {
let user = firebase.auth().currentUser;
let admin = null;
if (user) {
let token = await user.getIdTokenResult();
admin = token.claims.admin;
}
if (to.matched.some((res) => res.meta.requiresAuth)) {
if (user) {
if (to.matched.some((res) => res.meta.requiresAdmin)) {
if (admin) {
return next();
}
return next({ name: "Home" });
}
return next();
}
return next({ name: "Home" });
}
return next();
});
export default router;
How can I click through the page between sections?

You must switch your VueRouter from hash mode to history mode of routing - then hashtags will work but in a different way.
Your routes should not have a hash symbol # inside their path - instead, you should provide it under the hash attribute of the route link:
<router-link :to="{ name: pathName, hash: '#text' }">
Jump to content
</router-link>
But this alone is not enough. You also need to alter the scrollBehavior of the VueRouter:
import { routes } from './routes.js';
const router = new VueRouter({
routes,
scrollBehavior(to, from, savedPosition)
{
if (savedPosition)
{
return savedPosition;
}
if (to.hash)
{
return { selector: to.hash }; // <==== the important part
}
return { x: 0, y: 0 };
}
});

With a few research, I found two things that could help you.
First, this error is known and discussed on github vue-router issues page.
Second, I found that Workaround on npmjs.com, and you could probably give it a try.
EDIT
I found another solution to a similar problem here.
And from that page, I found a scrollBehavior example like this:
scrollBehavior: function (to) {
if (to.hash) {
return {
selector: to.hash
}
}
}
And if it still doesn't work, you could try to use
:to="{ name: 'Home', hash: 'fifthPage'}".

Related

Vue router - navigation guard is skipped when using router.push()

I have a Vue SPA with i18n and some views that require authentication via navigation guard.
When i am not authenticated and go to url via my browser:
examplepage.com/en/lockedpage
i get redirected to:
examplepage.com/en/login
which is good, however when i click a button that runs:
#click="$router.push(`/${$i18n.locale}/lockedpage`)"
i get in to the page even if i am not authenticated.
I want to get redirected to the login page if not authenticated
this is my router.js:
import Vue from 'vue';
import Router from 'vue-router';
import Home2 from './views/Home2.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';
import ErrorLanding from './views/ErrorLanding.vue'
import Root from "./Root"
import i18n, { loadLocaleMessagesAsync } from "#/i18n"
import {
setDocumentLang
} from "#/util/i18n/document"
Vue.use(Router);
const { locale } = i18n
export const router = new Router({
mode: 'history',
base: '/',
routes: [
{
path: '/',
redirect: locale
},
{
path: "/:locale",
component: Root,
children: [
{
name: 'Home',
path: '',
component: Home2,
},
{
name: 'Home2',
path: '/',
component: Home2,
},
{
name: 'Login',
path: 'login',
component: Login,
},
{
path: 'register',
component: Register,
},
{
path: 'lockedpage',
name: 'lockedpage',
webpackChunkName: "lockedpage",
meta: {authRequired: true},
component: () => import('./views/LockedPage.vue')
},
{
path: '*',
component: ErrorLanding,
name: 'NotFound'
}
]
}
],
router.beforeEach((to, from, next) => {
if (to.params.locale === from.params.locale) {
next()
return
}
const { locale } = to.params
loadLocaleMessagesAsync(locale).then(() => {
setDocumentLang(locale)
const publicPages = [ `/`, `/${locale}`, `/${locale}/`];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
const redirect = to.path;
if (authRequired && loggedIn === null) {
if(to.meta.authRequired === false) {
next();
}
else
next({ name: 'Login', query: { redirect: redirect } });
} else {
next();
}
})
next()
});
Why does my navigationguard skip when using router.push() ?
This issue started after adding i18n, with localeredirect. so all routes comes after a locale for example: /en/.../
As Estus points out in the comments, the issue is that the first thing you're checking for is if the locales match, and if they do you're calling next() and sending the user to the page.
As outlined here:
Make sure that the next function is called exactly once in any given pass through the navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors.
If you need to keep the locale check between the to and from pages, you could do something like:
if (to.params.locale === from.params.locale && loggedIn) {
next()
return
}
which will check if the loggedIn variable is truthy before pushing the user to the page they're trying to navigate to.
I believe just removing the if statement that checks if the locales match would also work.

Vue router guard triplicating navigation

I have a router guard beforeEach route to watch if there's user authenticated:
import Vue from "vue";
import VueRouter from "vue-router"
import Login from "../views/Login.vue"
import Home from "../components/Home.vue"
import Register from "../views/Register.vue"
import Dashboard from "../views/Dashboard.vue"
import Pricing from "../components/Pricing.vue"
import Invoices from "../components/Invoices.vue"
import { FirebaseAuth } from "../firebase/firebase"
Vue.use(VueRouter);
const routes = [
{
path: "*",
redirect: "/login",
},
{
path: "/dashboard",
name: "dashboard",
component: Dashboard,
children: [
{
path: "home",
name: "home",
component: Home,
},
{
path: "pricing",
name: "pricing",
component: Pricing,
},
{
path: "invoices",
name: "invoices",
component: Invoices,
}
],
meta: {
auth: true,
},
redirect: "home"
},
{
path: "/login",
name: "login",
component: Login,
},
{
path: "/register",
name: "register",
component: Register,
}
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
router.beforeEach((to, from, next)=>{
let user = FirebaseAuth.currentUser;
let auth = to.matched.some(record => record.meta.auth);
if (auth && !user) {
next('/login');
} else if (!auth && user) {
next('/dashboard/home');
} else{
next();
}
});
export default router;
When I perform logouts and logins there's an error about redundant navigation, however, I just assumed that it's ok if I just catch this.$router.push('/dashboard/home').catch(err => err); and move on without the console.log err. But creating an alert on component created() I've noticed that the thing is just more serious than what I thought, the component that shows the alert on created() it's showing it three times, and as I have a fetch for restore items on created(), that function is being triggered three times which is obviously not the performance wanted.
async created() {
alert("created")
this.credits = await fetchCredits(this.$firestore, this.$auth.currentUser);
let role = await getCustomClaimRole(this.$auth.currentUser);
this.subscription = role
? role.charAt(0).toUpperCase() + role.slice(1) + " " + "plan"
: "You haven't subscribed yet";
this.isLoading();
},
inside fetchCredits() is the console.log triggering three times
export const fetchCredits = async function (firestore, currentUser) {
// firestore collection of customers
const db = firestore.collection("customers");
/**
* Let's fetch the credits from the user:
*/
const credits = (await db.doc(currentUser.uid).get()).data();
if (credits !== "undefined") {
console.log(credits);
return credits.credits
} else {
return 0;
}
}
I think the problem is with the navigation guard, however, correct me if I'm wrong, but how to solve this?
I think that it has something to do with your router path:
{
path: "*",
redirect: "/login",
},
I have used Vue Router several times, but since I hadn't used wildcards before, I built a simplified Vue 2 CLI test application.
My router:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import Home from '#/components/stackoverflow/router-wildcard-match/Home'
import RouteOne from '#/components/stackoverflow/router-wildcard-match/RouteOne'
import RouteTwo from '#/components/stackoverflow/router-wildcard-match/RouteTwo'
import WildCard from '#/components/stackoverflow/router-wildcard-match/WildCard'
const routes = [
{
path: '/*',
name: 'wildcard',
component: WildCard
},
{
path: '/home',
name: 'home',
component: Home,
},
{
path: '/routeone',
name: 'routeOne',
component: RouteOne,
},
{
path: '/routetwo',
name: 'routeTwo',
component: RouteTwo,
},
]
export default new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
and my navbar component that routes programmatically:
<template>
<div class="navbar-sandbox">
<nav class="navbar navbar-expand-md navbar-light bg-light">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#" #click.prevent="navigate('home')">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" #click.prevent="navigate('routeone')">RouteOne</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" #click.prevent="navigate('routetwo')">RouteTwo</a>
</li>
</ul>
</nav>
</div>
</template>
<script>
export default {
data() {
return {
//currentRoute: 'home',
currentPath: 'home'
}
},
methods: {
// NOTE: Using route names work regardless of having wildcard path
// navigate(route) {
// if (route !== this.currentRoute) {
// this.currentRoute = route;
// this.$router.push({ name: route });
// }
// },
navigate(path) {
if (path !== this.currentPath) {
this.currentPath = path;
this.$router.push({ path: path });
}
}
}
}
</script>
As you can see in my code comments, when I programmatically route via the route names, it works even with having a wildcard path, but when I route via the actual route path, the routes are all intercepted by the wildcard.
My wildcard path is a bit different that yours, /* vs *.

Vue-Router language based route prefix

I'm using prerender-spa-plugin in order to prerender certain pages so I get better SEO from my Vue app.
My goal is to transform the way I'm currently using Vue-i18n, so I can base it on url param /lang. Examples: /en/home or /nl/home. With this, I would be able to pre-render depending on the language.
I created a prefixer function that adds to every parent route the optional param /:lang?. Here it is:
const withPrefix = (prefix: string, routes: RouteConfig[]): RouteConfig[] => routes.map((route): RouteConfig => {
// Avoiding mutations
const clonedRoute = { ...route };
// Every route except for '/'
if (clonedRoute.path !== '/') {
clonedRoute.path = prefix + clonedRoute.path;
}
return clonedRoute;
});
In Vue templates, I'm using:
<router-link :to="`/account`">
So I'm trying to manipulate the redirect to the next page according to the lang param.
First approach
The most logical one is (inside Router's beforeEach):
const { lang } = to.params;
const redirectTo = lang ? to.fullPath : `${fullToLang}${to.fullPath}`;
if (from.fullPath !== redirectTo) {
next({ path: redirectTo });
} else {
next();
}
But it enters in an endless loop because from is always the same.
Second approach
Using Router's base property.
import Vue from "vue";
import App from "./App.vue";
import VueRouter from "vue-router";
import HelloWorld from "./components/HelloWorld";
import Test from "./components/Test";
Vue.config.productionTip = false;
Vue.use(VueRouter);
const router = new VueRouter({
mode: "history",
base: "/en",
routes: [
{
path: ":lang?/",
component: HelloWorld,
beforeEnter: (to, from, next) => {
console.log(1);
next();
}
},
{
path: "/:lang?/nope",
component: Test,
beforeEnter: (to, from, next) => {
console.log(2);
next();
}
},
{
path: "/:lang?/*",
beforeEnter: (to, from, next) => {
console.log(to);
next("/nope");
}
}
]
});
new Vue({
render: h => h(App),
router
}).$mount("#app");
Or better, live:
https://codesandbox.io/embed/vue-template-0bwr9
But, I don't understand why it's redirecting to /en/nope only if the url is not found on the routes (last case). And more, would I have to create a new Router instance each time I want to change base?
Third approach
Wrapper component for router-link injecting :to based on this.$route.params.lang.
This would do it for navigation after the app is loaded but not at the first refresh/initialization.
So, how should I resolve this?
~ Solution ~
So yeah, first approach was the correct way to go but I missunderstood how Router behaves with next and redirects. The condition should be checking the to not the from.
const redirectTo = lang ? to.fullPath : `${fullToLang}${to.fullPath}`;
if (to.fullPath !== redirectTo) {
// Change language at i18n
loadLanguageAsync(toLang as Language);
next({ path: redirectTo });
return;
}
I am not entirely sure what you are asking. But I assume you want to prefix your navigations with the current language param (../en/..) if they do not already have one?
You could resolve this with a beforeEach() hook and only redirecting if there is no lang param present.
const { lang } = to.params
if(!lang) {
next({ path: redirectTo })
}
next()
If that's not what you want please clarify and I'll edit my answer
Something like this? The assumption is that the new path starts /[lang]/...
as a note - there are still errors when routing e.g. /:lang/bar -> /foo/bar
Vue.lang = 'en'
function beforeEnter(to, from, next){
if ((new RegExp(`^/${Vue.lang}$`))
.test(to.path)
||
(new RegExp(`^/${Vue.lang}/`))
.test(to.path))
{
next();
} else {
next({path: `/${Vue.lang}${to.path}`})
}
};
Vue.mixin({
beforeRouteEnter: beforeEnter
})
const Foo = { template: '<div>foo - {{$route.path}}</div>' }
const Bar = { template: '<div>bar - {{$route.path}}</div>' }
const Root = { template: '<div>Root - {{$route.path}}</div>' }
const Invalid = { template: '<div>404</div>' }
const routes = [
{ path: '/:lang/foo', component: Foo },
{ path: '/:lang/bar', component: Bar },
{ path: '/:lang/*', component: Invalid },
{ path: '/:lang', name: 'Home', component: Root },
// some weird issue that prevents beforeRouteEnter ? so redirect, but else next is needed
{ path: '/', redirect: to => `/${Vue.lang}`}
]
const router = new VueRouter({
routes
})
new Vue({
data(){
return {
pLang: Vue.lang,
}
},
computed: {
lang: {
get(){
return this.pLang
},
set(val){
Vue.lang = val
this.pLang = val
}
}
},
router,
}).$mount('#app');
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
{{lang}}
<select v-model="lang">
<option value="en">en</option>
<option value="cn">cn</option>
</select>
<!-- use router-link component for navigation. -->
<!-- specify the link by passing the `to` prop. -->
<!-- `<router-link>` will be rendered as an `<a>` tag by default -->
<router-link to="/">Root</router-link>
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
<router-link to="/foo/bar">Go to Foo/Bar - not defined</router-link>
</p>
<!-- route outlet -->
<!-- component matched by the route will render here -->
<router-view></router-view>
</div>

Pass var to vue router in meta

I have a route created with vue-router.
{
path: '/events/:id',
component: Event,
name: 'Event',
meta: {
title: 'Design Web'
}
},
In "meta", I give it the name of my page.
I can call the title of my page by doing this: $route.meta.title
But now, I'm facing a problem. In the title of my page, I would like to pass a variable (the name of my event).
meta: {
title: $nameOfEvent
}
How to do ?
Thank you
It is possible if you define the title attribute as a function :
{
meta: { title: route => { /* return custom title based on route, store or anything */ } }
}
and
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title(to);
}
next();
})
Codepen: https://codepen.io/anon/pen/roRmdo?editors=1111 (you need to inspect inner iframe to see the title change).
or create a directive:
Vue.directive('title', {
inserted: (el, binding) => document.title = binding.value,
update: (el, binding) => document.title = binding.value
})
Then use that directive on the router-view component:
<router-view v-title="title" ></router-view>
Component:
export default {
data(){
return {
title: 'This will be the title'
}
}
}
Source: https://github.com/vuejs/vue-router/issues/914

Vue router navigation gaurd from within the component

I use vuex from centralized state management
in my vuex store.js i store the login status as a boolean value like below
export const store = new Vuex.Store({
state: {
loggedIn: false,
userName: 'Guest',
error: {
is: false,
errorMessage: ''
}
},
getters: {
g_loginStatus: state => {
return state.loggedIn;
},
g_userName: state => {
return state.userName;
},
g_error: state => {
return state.error;
}
}
)};
When the user logs in i set the loginstatus to true and remove the login button and replace it with log out button
everything works fine but the problem is when the user is logged in and if i directly enter the path to login component in the search bar i am able to navigate to login page again
I want to preent this behaviour
If the uses is logged in and searches for the path to loginpage in the searchbar he must be redirected to home page
I have tried using beforeRouteEnter in the login component
But we do not have acess to the this instance since the component is not yet loaded
So how can i check for login status from my store
my script in login.vue
script>
export default{
data(){
return{
email: '',
password: ''
};
},
methods: {
loginUser(){
this.$store.dispatch('a_logInUser', {e: this.email, p: this.password}).then(() =>{
this.$router.replace('/statuses');
});
}
},
beforeRouteEnter (to, from, next) {
next(vm => {
if(vm.$store.getters.g_loginStatus === true){
//what shall i do here
}
})
}
}
It is much better to put the navigation guards in routes not in pages/components and call the state getters on route file.
// /router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'
// Protected Pages
import Dashboard from '#/views/dashboard'
// Public Pages
import Dashboard from '#/views/login'
Vue.use(Router)
// If you are not logged-in you will be redirected to login page
const ifNotAuthenticated = (to, from, next) => {
if (!store.getters.loggedIn) {
next()
return
}
next('/') // home or dashboard
}
// If you are logged-in/authenticated you will be redirected to home/dashboard page
const ifAuthenticated = (to, from, next) => {
if (store.getters.loggedIn) {
next()
return
}
next('/login')
}
const router = new Router({
mode: 'history',
linkActiveClass: 'open active',
scrollBehavior: () => ({ y: 0 }),
routes: [
{
path: '/',
redirect: '/dashboard',
name: 'Home',
component: Full,
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: Dashboard,
beforeEnter: ifAuthenticated
},
]
},
{
path: '/login',
name: 'Login',
component: Login,
beforeEnter: ifNotAuthenticated
},
{
path: '*',
name: 'NotFound',
component: NotFound
}
]
})
export default router
You can also use vue-router-sync package to get the value of store values
You can redirect the user to the home page or some other relevant page:
mounted () {
if(vm.$store.getters.g_loginStatus === true){
this.$router('/')
}
}
beforeRouteEnter (to, from, next) {
next(vm => {
if(vm.$store.getters.g_loginStatus === true){
next('/')
}
})
}
From the docs:
next: Function: this function must be called to resolve the hook. The action depends on the arguments provided to next:
next(): move on to the next hook in the pipeline. If no hooks are left, the navigation is confirmed.
next(false): abort the current navigation. If the browser URL was changed (either manually by the user or via back button), it will be reset to that of the from route.
next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.