Vue router guard triplicating navigation - vue.js

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 *.

Related

Vue 3 dynamic components at router level

Dynamic imports is needed for me, eg. i have 10 layouts, but user only visited 3 layouts, I should not import all of the layouts, since its consumed unnecessary resources.
Since its dynamic import, each time i switch between Login & Register path <RouterLink :to"{name: 'Login'}" /> & <RouterLink :to"{name: 'Register'}" />, I got rerender or dynamic import the layout again.
My question is what is the better way to handle it, without rerender or dynamic import the layout again? Or can I save the dynamic import component into the current vue 3 context?
App.vue this is my app with watching the router and switch the layout based on route.meta.layout
<template>
<component :is="layout.component" />
</template>
<script>
import DefaultLayout from "./layout/default.vue";
import {
ref,
shallowRef,
reactive,
shallowReactive,
watch,
defineAsyncComponent,
} from "vue";
import { useRoute } from "vue-router";
export default {
name: "App",
setup(props, context) {
const layout = shallowRef(DefaultLayout);
const route = useRoute();
watch(
() => route.meta,
async (meta) => {
if (meta.layout) {
layout = defineAsyncComponent(() =>
import(`./layout/${meta.layout}.vue`)
);
} else {
layout = DefaultLayout;
}
},
{ immediate: true }
);
return { layout };
},
};
</script>
router/index.js this is my router with layout meta
import { createRouter, createWebHistory } from "vue-router";
import Home from "#/views/Home.vue";
import NotFound from "#/views/NotFound.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/login",
name: "Login",
meta: {
layout: "empty",
},
component: function () {
return import(/* webpackChunkName: "login" */ "../views/Login.vue");
},
},
{
path: "/register",
name: "Register",
meta: {
layout: "empty",
},
component: function () {
return import(/* webpackChunkName: "register" */ "../views/Register.vue");
},
},
{ path: "/:pathMatch(.*)", component: NotFound },
];
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_GITLAB_BASE_PATH),
routes,
scrollBehavior(to, from, savedPosition) {
// always scroll to top
return { top: 0 };
},
});
export default router;
You could use AsyncComponent inside the components option and just use a computed property that returns the current layout, this will load only the current layout without the other ones :
components: {
layout1: defineAsyncComponent(() => import('./components/Layout1.vue')),
layout2: defineAsyncComponent(() => import('./components/Layout2.vue')),
},
Had this issue and Thorsten Lünborg of the Vue core team helped me out.
add the v-if condition and that should resolve it.
<component v-if="layout.name === $route.meta.layout" :is="layout">

Navigating to route returns Maximum call stack size exceeded

I want to navigate the user to /home once he clicks on a box, but currently the redirection does not work and I get this error:
Maximum call stack size exceeded
What is causing this error and how can I fix it?
here is my relevant html:
<router-link to="/home"> <md-button class="view-captable-btn" v-for="item in capTables" :key="item.contractAddress" v-model="capTables" #click="getCapTableId(item.contractAddress, item); getCaptableName(item.label, item)" >
<p>{{item.label}}</p>
<p class="contract-address-p"> {{item.contractAddress}}</p>
</md-button>
</router-link>
my method:
getCapTableId(contractAddress, item) {
let address = item.contractAddress
store.commit('saveCapTableAddress', address)
},
and my protected route logic :
import Vue from 'vue'
import Router from 'vue-router'
import firebase from "firebase/app"
import "firebase/auth"
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/unauthorized',
name: 'Unauthorized',
component: Unauthorized
},
{
path: '/activity-log',
component: ActivityLog,
name: 'ActivityLog'
},
{
path: '/captables',
name: 'CapTables',
component: CapTables,
meta: {
requiresAuth: true
},
},
{
path: '*',
component: NotFound,
name: 'NotFound',
},
],
mode: 'history'
})
router.beforeEach((to, from, next) => {
const currentUser = firebase.auth().currentUser
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
if(requiresAuth && !currentUser) next('/unauthorized')
else if (!requiresAuth && currentUser) next('home')
else next()
});
export default router
I tried using pushing the route with this.$route.push('/home') still doesn' work.

$route.name and/or $router.currentRoute.name return null when I'm calling in CREATED hook

Here is my router
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/boys',
name: 'Boys',
component: () => import(/* webpackChunkName: "boys" */ '../views/Boys.vue')
},
{
path: '/girls',
name: 'Girls',
component: () => import(/* webpackChunkName: "girls" */ '../views/Girls.vue')
}
]
const router = new VueRouter({
mode: 'history',
linkExactActiveClass: "active__nav",
routes
})
export default router
Here is my whole component, I'm putting whole code just in case
<template>
<div class="nav">
<ul>
<router-link v-for="(menuItem, i) in menu"
:key="menuItem.name"
:to="{name : menuItem.route}"
tag="li">
<p class="menu__item__name"
#mouseover="ft_hover($event, i, 20)"
#mouseleave="ft_hover($event, i, 0)"
#click="ft_click($event, i)">
{{ menuItem.title }}
</p>
<div class="animate__link" :style="menuItem.styleNavBar"></div>
</router-link>
</ul>
</div>
</template>
<script>
import MenuBar from "#/store/menu.js"; // importing menu with data
export default {
namespaced: true,
name: "TheNav",
data(){
return {
menu: MenuBar.navBar
}
},
methods: {
ft_click(e, i) // it just do animation on clicked Nav
{
this.menu.forEach( (el) => {
el.styleNavBar.width = 0 + '%';
el.active = false;
})
this.menu[i].active = true;
this.menu[i].styleNavBar.width = 80 + '%';
console.log(this.$route.name); // here I tried console log $route on click on menu
},
ft_hover(e, i, width) // it just do animation on hovered Nav
{
if(this.menu[i].active !== true)
this.menu[i].styleNavBar.width = width + '%';
}
},
created()
{
console.log(this.$router.currentRoute.name); // here is where I have issue
}
}
I want to get router name when I'm loading a page for a first time. And $router.currentRoute.name or $route.name works perfectly fine if I load main page first as localhost:8080 it returns 'Home'. But if I load localhost:8080/girls or localhost:8080/boys it returns 'null'. I tried debug it. I have a method ft_click which do some animation on clicked menu, and for my surprise it prints out router name right on every click what I do on navigation, except it prints it ONE STEP BEHIND, so if I was on Home page and clicked on Boys it prints Home, then if I click Girls it prints Boys. I don't know if that related to my issue tho, but it gives at least an idea that with my routes everything is good is just I guess I dont know something, thats why it doesnt work....
also in case if #store/menu is needed
export default {
navBar: [{
title: 'Main',
route: 'Home',
styleNavBar: {
background: "#d1d8e0",
width: 0 + '%'
},
active: false
},
{
title: 'Girls',
route: 'Girls',
styleNavBar: {
background: "#f8c291",
width: 0 + '%'
},
active: false
},
{
title: 'Boys',
route: 'Boys',
styleNavBar: {
background: "#34495e",
width: 0 + '%'
},
active: false
}
]}

Vue - Keep default Router-View alive when change another named view

Situation:
I use, beside of the default route-view, a named route-view. I want to keep the DEFAULT route-view alive when I call the ArticleComponent, but as you can see, you can call the ArticleComponent from 2 different routes/components. You can find a fiddle link under the code snippet.
What I want to do:
If I open the ArticleComponent from ListingComponent, then ListingComponent should stay alive in the default route-view.
If I call the ArticleComponent from the FeedComponent, then the FeedComponent should stay alive in the default route-view.
My code:
const HomeComponent = {
template: '<h4>Home</h4>'
};
const FeedComponent = {
template: `<div>
<h4>FeedComponent</h4>
<router-link to="/article/1">Article 1</router-link> -
<router-link to="/article/2">Article 2</router-link>
</div>`
};
const ListingComponent = {
template: `<div>
<h4>ListingComponent</h4>
<router-link to="/article/1">Article 1</router-link> -
<router-link to="/article/2">Article 2</router-link> -
<router-link to="/article/3">Article 3</router-link>
</div>`
};
const ArticleComponent = {
template: `<h4>Article {{ $route.params.id }}</h4>`
};
const routes = [
{
path: '/',
component: HomeComponent
},
{
path: '/feed',
component: FeedComponent
},
{
path: '/listing',
component: ListingComponent
},
{
path: '/article/:id?',
components: {
default: FeedComponent, // <--- dynamically
secondary: ArticleComponent
}
}
];
const router = new VueRouter({
routes
});
new Vue({
el: '#app',
router
});
Fiddle:
https://jsfiddle.net/kvnvooo/b589uvLt/9/
You can use Navigation guards to alter default component dynamically...
{
path: '/article/:id?',
components: {
default: FeedComponent,
secondary: ArticleComponent
},
beforeEnter: (to, from, next) => {
if(from.fullPath === '/listing') {
to.matched[0].components.default = ListingComponent
} else if(from.fullPath === '/feed') {
to.matched[0].components.default = FeedComponent
}
next();
}
}
https://jsfiddle.net/dhmLby6f/7/

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>