Element UI NavMenu gets out of sync with current route - vue.js

I'm using the Element UI NavMenu with :router="true". It is working fine when I click on menu links (route changes and active menu item changes). The issue I'm having is that when I click on the browser navigation buttons (back and forward), the route and component change, but the NavMenu active tab does not change.
Is there an easy way to make sure the NavMenu and current route stay in sync with each other when using the browser navigation buttons? I'm using vue-router with mode: 'history'. I would have thought that this would be handled automatically.

I originally tried to implement this answer with no luck. I now have a working solution for this issue. In my navigation component, I have an el-menu with :router="true" and:default-active="activeLink"`.
Since I have a fairly simple Vue application, I did not want to loop over my router paths and build the NavMenu dynamically. This is a good practice, but I wanted to understand how it works at a basic level first.
From the element-ui docs, default-active controls the index of currently active menu. I added activeLink as a data property:
data() {
return {
logo: logo,
activeLink: null,
}
},
and then added a watch property as described in the gist linked above:
watch: {
$route (to, from) {
this.activeLink = to.path;
}
},
The part I was missing was that the index and the route properties of the el-menu-item need to be the same. Also, we can add a mounted method to make sure that the correct nav link is made active no matter what path we load the app from:
mounted: function(){
this.activeLink = this.$route.path;
},
That fixed the issue of the NavMenu getting out of sync when I use browser navigation buttons.

This was a pain to get to work. I couldn't get beforeRouteUpdate() to work at all, and :default-active="$route.path" almost works, but not if you have parameters for your routes. My current solution is to name all of my routes, and add menu items where the index is the name. Then the default-active value can just be taken from $route.name.
<el-menu :default-active="$route.name" #select="menuSelect">
<el-menu-item index="summary">
<span slot="title">Summary</span>
</el-menu-item>
<el-menu-item index="memory">
<span slot="title">Memory Overview</span>
</el-menu-item>
...
</el-menu>
And in your component:
public menuSelect(index: string) {
this.$router.push({
name: index,
});
}
You can also avoid the annoying error Navigating to current location ("summary") is not allowed like this:
public menuSelect(index: string) {
if (this.$route.name !== index) {
this.$router.push({
name: index,
});
}
}

Related

Why does the browser display cached Vue.js view on route/url change?

I have a homepage with <router-link> tags to views. It is a simple master/detail relationship where the Homepage is a catalogue of products and the Product detail page/view shows information on each item.
When I first launch the website and click on an item on the Homepage view (e.g. URL: http://localhost:8080/100-sql-server-2019-licence), the Product view gets loaded and the product detail loads fine.
If I then press the back button in the browser to return to the Homepage and then click on a different Product (e.g. URL: http://localhost:8080/101-oracle-12c-licence), the URL in the browser address bar changes but I get the previous product's information. Its lightning quick and no new network calls are done which means its just showing a cached page of the previous product. If I then hit the refresh button while on that page, the network call is made and the correct product information is displayed.
I did a search online but couldn't find this problem described on the search results. Could anyone point me in the right direction of how to cause a refresh/re-render of a route when the route changes?
What is happening
vue-router will cache your components by default.
So when you navigate to the second product (that probably renders the same component as the first product), the component will not be instantiated again for performance reasons.
From the vue-router documentation:
For example, for a route with dynamic params /foo/:id, when we
navigate between /foo/1 and /foo/2, the same Foo component instance
will be reused.
The easy (but dirty) fix
The easy -but hacky and not recommended - way to solve this is to give your <router-view /> a key property, e.g.:
<router-view :key="$route.fullPath" />
This will force vue-router to re-instantiate the view component every time the url changes.
However you will loose all performance benefits you would normally get from the caching.
Clean fix: properly handling route changes
The clean way to solve this problem is to react to the route-change in your component (mostly this boils down to moving ajax calls from mounted into a $route watcher), e.g.:
<script>
export default {
data() {
return {
productDetails: null,
loading: false
};
},
watch: {
'$route': {
// with immediate handler gets called on first mount aswell
immediate: true,
// handler will be called every time the route changes.
// reset your local component state and fetch the new data you need here.
async handler(route) {
this.loading = true;
this.productDetails = null;
try {
// example for fetching your product data
const res = await fetch("http://give.me.product.data/" + encodeURIComponent(route.params.id));
this.productDetails = await res.json();
} finally {
this.loading = false;
}
}
}
}
};
</script>
Alternative: Navigation Guards
Alternatively you could also use vue-routers In-Component Navigation Guards to react to route changes:
<script>
export default {
async beforeRouteUpdate (to, from, next) {
// TODO: The route has changed.
// The old route is in `from`, the new route in `to`.
this.productData = await getProductDataFromSomewhere();
// route will not change before you haven't called `next()`
next();
}
};
</script>
The downside of the navigation guards is that you can only use them directly in the component that the route renders.
So you can't use navigation guards in components deeper within the hierarchy.
The upside is that the browser will not view your site before you call next(), which gives you time to load the data necessary before your route is displayed.
Some helpful ressources
Vue Router Navigation Guards Documentation
vue-router github issue
Similar Question about vue-router component reuse on stackoverflow

Vue URL Updating but Component is not

Having an issue with my url updating but the page not.
From the home page, I display a list of projects. Clicking a project will take you to "website.com/project/project-1" and everything works as intended.
However, at the bottom of that page, I again show a list. This list is the same as homepage, with same functionality. But the problem is, is that it will update the url to "website.com/project/project-2" but the page will not re-render or change.
An example of my code
My current router-path of the component.
path: '/project/:project_slug',
name: 'ProjectPage',
component: ProjectPage
My Router Link from the project page to the new project page
<router-link :to="{ name: 'ProjectPage', params: {project_slug: projectHighlightSlug} }">
<h4 class="header-17 semibold">{{projectTitle}}</h4>
</router-link>
Update
This is my current method/watch section
methods: {
goToProject() {
this.$router.push({
name: 'ProjectPage',
params: {project_slug: this.projectHighlightSlug}
})
},
},
watch:{
// eslint-disable-next-line no-unused-vars
'$route'(to, from) {
this.goToProject();
}
}
However, the to,from is "defined but never used" and clicking my button to call goToProject() gives me the error;
"You may have an infinite update loop in watcher with expression "$route""
As explained in the Vue Router docs, when the url the user navigates to uses the same component, it uses the same instance of that component. The docs therefore recommend to listen to $route changes or to use the beforeRouteUpdate navigation guard.
You need to watch the routes to update your page. see code below
watch:{
'$route' (to, from) {
this.goToProject()
// call your method here that updates your page
}
},
source dynamic route matching

Nuxt not adding bodyAttr on refresh / ssr

I have the following code in one of my layouts.
bodyAttrs: {
class: this.$store.state.user.theme + '-scheme'
}
unfortunately, i am using an old bootstrap theme css. I DO NOT want to redo it as such i have to figure out some work arounds.
The theme color for the bootstrap theme attaches itself to the body tag. Unfortunately the body tag is a no no in nuxt.
What i have noticed is, that upon refresh the page is rendered with the base state value.
example :
//store/index.js <- not modular
var state = () => ({
user: {
id: 0,
avatar: 'default.jpg',
status: 'offline',
nickname: 'Guest',
admin: false,
theme: 'brownish' //-> this is the value
}
})
The entire page renders with the users details but the theme variable is not placed into the render. If i go to another page(ajax routing), the page is updated with the correct color.
Essentially the page loads brown and then on subsequent pages it will load blue. If the page is refreshed then the color reverts to brown and then on subsequent pages it will turn blue again.
How can i get the SSR to display the correct bodyAttr?
I am using vuex-persistedstate and cookies both are client side
The nuxt server does not have any sessions as this is handled on a separate domain(api)
I couldn't find a pure vuejs way to do this so i made a fix.
This is technically a bad idea as it is directly modifying the DOM which is a no-no in vue. However, vue doesnt use reactivity on the body tag. Therefore i think it is fairly safe to use. Also, using vuex-presistedstate will ensure that vue set the proper state variable on subsequent loads..
//This is layouts/default.vue
head() {
return {
bodyAttrs: {
class: this.$store.state.user.theme + '-scheme'
}
}
},
mounted() {
window.onNuxtReady(() => {
document.body.className = this.$store.state.user.theme + '-scheme'
})
}
Are you using the cookies based version of vuex-persistedstate? Customize Storage
Remember to enable the ssr mode for the plugin in nuxt.config.js

Vue router reloading the current route

Without reloading the whole page I need to reload the current route again (Only a component reload) in a vue app.
I am having a path in vue router like below,
{
path: "/dashboard",
name: "dashboard",
component: loadView("Dashboard"),
},
When user clicks on the Dashboard navigation item user will be redirected to the Dashboard page with vue router programmatic navigation
this.$router.push({ name: "dashboard" });
But when user already in the dashboard route and user clicks the Dashboard nav item again nothing happens. I think this is vue router's default behaviour. But I need to force reload the Dashboard component (Not to refresh the whole page).
I can't use beforeRouteUpdate since the router is not updated. Also I have tried the global before guards like beforeEach. But it is also not working.
How can I force reload the dashboard component without reloading the whole page?
It can be done in two ways.
1) Try doing vm.$forceUpdate(); as suggested here.
2) You can take the strategy of assigning keys to children, but whenever you want to re-render a component, you just update the key.
<template>
<component-to-re-render :key="componentKey" />
</template>
<script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
</script>
Every time that forceRerender is called, the prop componentKey will change. When this happens, Vue will know that it has to destroy the component and create a new one.
What you get is a child component that will re-initialize itself and “reset” its state.
Not mentioned here, but as the offered solutions require a lot of additional work just to get the app to render correctly, which imo is a brittle solution.. we have just implemented another solution which works quite well..
Although it is a total hack.
if (this.$route.name === redirect.name) {
// this is a filthy hack - the vue router will not reload the current page and then have vue update the view.
// This hack routes to a generic page, then after this has happened the real redirect can happen
// It happens on most devices too fast to be noticed by the human eye, and in addition does not do a window
// redirect which breaks the mobile apps.
await this.$router.push({
name: RouteNames.ROUTE_REDIRECT_PLACEHOLDER
});
}
... now continue to do your normal redirect.
Essentially, redirect to a placeholder, await the response but then immediately continue to another page you actually wanted to move toward

Vue router not navigating

I'm having problems getting VueRouter to navigate.
Within my app some pages work fine, and with identical code, other pages the routing doesn't work / page doesn't update navigate.
Are there some gotchas with the router? Like you cannot call the router from within components or... ?
named route in my app
export default new VueRouter({
routes: [
{
path: '/grams/one/:cname',
component: GramsOne,
name: "gramsOne"
},
...
Then inside a component on a page:
<q-btn v-for='(rel, key) in gram.relatedItems' :key='key'
color='secondary'
#click="goGram(rel)"
>
{{rel.cn}}
</q-btn>
// later in the script:
methods: {
goGram(gram) {
let newRoute = {
name: 'gramsOne',
params: {cname: gram.cname}
}
console.log('goGram', newRoute)
this.$router.push(newRoute)
}
},
Elsewhere on the same page, simple routes work.
The URL address will get updated in the browser.
I see the right console log with route info
But the page/content will not change.
Once the URL bar has been updated, I can hit ctrl-R and the new page will get loaded. So the destination route is working fine.
From other pages in the same app I can use the same route to target new destination and loads fine.
This is also loading with the same URL and just a query param different that is causing the problem.
/app/items/foo
/app/items/bar
where bar is a type of some property /app/items/:foo
I have tried various combinations of named routes, routes using etc, and can't really see a pattern.
"vue": "~2.5.0",
"vue-resource": "^1.3.4",
"vue-router": "^2.7.0"
Thanks any hints!
The Component in this case is the same, so vue will reuse the instance.
The this.$route in the component will change but created(), beforeMounted() and mounted() hooks won't be callled.
Which is probably where you use the this.$route.params.cname
To force vue to create a new component instance you can set a unique key on the like <router-view :key="$route.fullPath">
Another options is to react to changes in the $route with a watcher:
watch: {
"$route.params.cname": {
handler(cname) {
// do stuff
},
immediate: true
}
}
Router can be called within a component just fine. In fact, you usually call router from the component.
From the code snippets provided, at one point in the code you use cn, then at other point you use cname. That might be the problem why it never quite work for you.
I create a codesandbox to recreate your scenario, and besides that minor naming mentioned above, things seem to work just fine.