How do I router-link to the same page with an updated query & reload the page? [duplicate] - vue.js

As part of my Quasar app, I have the following route:
import { RouteRecordRaw} from 'vue-router'
import { uid } from 'quasar'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: () => {
console.log('matched /')
return {path: `/${uid()}`}
}
},
{
path: '/:uuid',
component: () => import('pages/User.vue')
// component: User,
},
];
export default routes;
This works fine when going to /: the URL is changed to /73a219e5-2cf2-4dd0-8... and User.vue is executed (specifically there a fetch inside that retrieves some data based on the :uuid parameter.
If I force a route from within a component (User.vue for instance), via
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/')
I do see that the URL changes to a new UUID but User.vue is not executed. Specifically, a reference to route.params.uuid where const route = useRoute() is not reactive.
Is this normal (= I have to look for anther way to trigger), or is there a misuse (erroneous use) on my side?

The core of the issue is that you're (re)using the same component for rendering the page you're navigating from and the page you're navigating to.
By design, Vue optimises DOM rendering and will reuse the existing component instance. This means certain hooks won't be triggered (e.g: mounted, created, etc...) when changing route.
To force Vue into creating a different component instance when the route changes, use the current route's .fullPath as key on <router-view>:
<template>
...
<router-view :key="route.fullPath"></router-view>
...
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute();
</script>

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.

Why is a route not reactive?

As part of my Quasar app, I have the following route:
import { RouteRecordRaw} from 'vue-router'
import { uid } from 'quasar'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: () => {
console.log('matched /')
return {path: `/${uid()}`}
}
},
{
path: '/:uuid',
component: () => import('pages/User.vue')
// component: User,
},
];
export default routes;
This works fine when going to /: the URL is changed to /73a219e5-2cf2-4dd0-8... and User.vue is executed (specifically there a fetch inside that retrieves some data based on the :uuid parameter.
If I force a route from within a component (User.vue for instance), via
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/')
I do see that the URL changes to a new UUID but User.vue is not executed. Specifically, a reference to route.params.uuid where const route = useRoute() is not reactive.
Is this normal (= I have to look for anther way to trigger), or is there a misuse (erroneous use) on my side?
The core of the issue is that you're (re)using the same component for rendering the page you're navigating from and the page you're navigating to.
By design, Vue optimises DOM rendering and will reuse the existing component instance. This means certain hooks won't be triggered (e.g: mounted, created, etc...) when changing route.
To force Vue into creating a different component instance when the route changes, use the current route's .fullPath as key on <router-view>:
<template>
...
<router-view :key="route.fullPath"></router-view>
...
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute();
</script>

vuejs loading component dynamically depending on route component

I'm a newbie to Vuejs and I'm wondering how I can load the component to App.vue depending on the route visited.
In my router -> index.js I have:
import { createRouter, createWebHistory} from 'vue-router'
import Events from '../views/Events.vue'
const routes = [
{
path: '/',
name: 'events',
component: Events
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
In my App.vue I have a template that I want to use for the entire website, so I thought to load the template here instead of loading it into each component.
My template has 1 box in the center of the screen, so the content changes depending on which route the user goes to.
Instead of calling the component content from the routes file, is there a better way to load it dynamically into App.vue depending on the route visited?
I could hard code it in App.vue with something like:
<template>
<Events />
</template>
<script>
import Events from './views/Events.vue'
export default defineComponent({
components: {
Events
},
</script>
But that won't solve my problem about the Events component being loaded dynamically depending on which route is visited. Is there a way to access the underlying component with something like this.$route to load the component instead of hard coding it.
The router-view component will render the component based on its path :
<template>
<router-view/>
</template>
<script>
export default defineComponent({
})
</script>

Vue-router reloads page and I lose my state, how do i avoid this?

I have a form divided in 5 components and the user can navigate through them via steppers (I'm using vue-material for my project). I use vue-router for that. However, I'm having a serious issue here: components lose all the information in the store (I'm using vuex) when they come back to a route they already filled. So to make it clear: if a user fills the first step of the form and then goes to step two, when he wants to come back to step one data is no longer available and the form is totally empty (and the state in vuex is also reset). What am i doing wrong?
import Vue from 'vue'
import Router from 'vue-router'
import Projet from '#/components/Fiches/Projet/Projet'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Projet
},
//other routes here
]
})
And this is the html code
<template>
<div class="project-steppers">
<md-steppers md-dynamic-height md-alternative>
<md-step id="first" to="/Projet" md-label="Projet" />
// other steps here
</md-steppers>
</div>
</template>
And an example of one of the inputs I use:
<md-field>
<label for="project-name">Nom du projet</label>
<md-input id="project-name"
v-model="project.projectName"
name="project-name"
#change="updateProjectName"/>
</md-field>
[...]
methods: {
updateProjectName () {
this.$store.commit(projectStore.MUTATE_PROJECTNAME, this.project.projectName)
}
More information: when I fill the different inputs I see that the store is updated with the new values, so the mutation is working.
First of all, Vuex does not store data in the browser - just in memory. That means that you could either install a third party plugin such as vuex persisted state or write your own methods to set and get the items from your storage, e.g.:
const storage = localStorage.getItem('key');
new Vuex({
state: {
yourProp: storage ?
? JSON.parse(storage.yourDataKey)
: 'default-value'
},
actions: {...}
mutations: {...}
})
I think to should use router-link or $router.push().
Vue:
import Vue from 'vue'
import Router from 'vue-router'
import Projet1 from '#/components/Fiches/Projet/Projet1'
import Projet2 from '#/components/Fiches/Projet/Projet2'
import Projets from '#/components/Fiches/Projet/Projet' //with props
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Projet1 // default project
},
{
path: '/Projet1', // url for the same component
name: 'Projet1',
component: Projet1
},
{
path: '/Project2',
name: 'Projet2', // url for the another component
component: Projet2
},
{
path: '/Project/:id',
name: 'Projets', // url for a component with props
component: Projet,
props: true
}
]
})
HTML: A way to call Projet without reloading with router-link
<template>
<router-link to="/Home"></router>
<router-link to="/Projet1"></router>
<router-link to="/Projet2"></router>
</template>
js: I would add a router push
updateProjectName () {
this.$store.commit(projectStore.MUTATE_PROJECTNAME, this.project.projectName)
this.$router.push('/' + this.project.projectName)
}
Your question looks like the issue opened by kristianmandrup:
menu or tabs with router links!?

vue js two SPAs

I am building a web app with two layouts (for login page, and dashboard). Each of them are represented as SPA application, so each has router-view. The main problem is 'How to connect them and redirect from one to another?'.
I have a App.vue - check if user is authorized. if yes - redirect to Dashboard.vue, else - redirect to Login.vue. Each of them has there own router-view.
An SPA should be a single html file which serves up your app and all the routes, so the basic structure should be:
HTML
<div id="app">
</div>
<!-- bundled file -->
<script src="app.js"></script>
app.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import App from './components/App.vue' // import Base component
// Import views to register with vue-router
import Login from './components/views/Login.vue'
import Dashboard from './components/views/Dashboard.vue'
const guard = function(to, from, next) {
// Check if user is logged in (you will need to write that logic)
if (userIsLoggedIn) {
next();
} else {
router.push('/login');
}
};
const routes = [{
path: '/login',
component: Login
},{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
guard(to, from, next); // Guard this route
}
}]
const router = new VueRouter({
mode: 'history', // history mode
routes
})
new Vue({
el: '#app',
router,
render: h => h(App) // mount base component
})
App.vue
<template>
<div>
<!-- Your layout -->
<!-- All views get served up here -->
<router-view></router-view>
</div>
</template>
I haven't tested that, but in this scenario every view component gets served up by App.vue which is mounted on the main vue instance. You then use the beforeEach guard to check that the user is logged in, if they are then you call next() which takes them to the route, if they are not then you redirect them to login.
vue-router has the ability to create custom guards for any route. You do not need 2 separate applications, just some safety with the routes in your router.
https://router.vuejs.org/en/advanced/navigation-guards.html
Your guard could be a function that checks for authentication.
Here's a full implementation tutorial from Auth0: https://auth0.com/blog/vuejs2-authentication-tutorial/