In VueJS, I am showing the loader on each route as:
router.beforeEach((to, from, next) => {
store.commit('loading', true);
next();
})
But if server loads the page in less than one second then it looks weird to show loader for this request, for just one sec.
What I want to wait for some time let just say 2sec or maybe 3sec and after all, if the page is not loaded yet then show loader otherwise not. So for this, I put setTimeout as:
router.beforeEach((to, from, next) => {
setTimeout(() => {
store.commit('loading', true);
}, 500);
next();
})
Now the loader is always shown never goes then I also tried to move next() statement into setTimeout but then the page first waits for 500 mili-sec then the loader shows up and then hides suddenly and page loads.
I want to make it in a better way, any suggestions?
I think you are not understanding the Vue Router Navigation Guards. According to Vue Router Docs:
Global before guards are called in creation order, whenever a navigation is triggered. Guards may be resolved asynchronously, and the navigation is considered pending before all hooks have been resolved.
In simple words, just show the loader in beforeEach as you are doing:
store.commit('loading', true);
And just hide it in afterEach as:
store.commit('loading', false);
That's it.
Don't add setTimeout in afterEach.
Hope it will help.
so about your question. As of now you're only delaying commiting 'loading' mutation by 500ms. To answer your question you should do something like that:
router.beforeEach((to, from, next) => {
store.commit('loading', true);
setTimeout(() => {
store.commit('loading', false);
}, 500);
next();
})
That will delay commiting store.commit('loading', false); by 500ms. The question is do you really want to falsely delay loading of a component. Why not use transitions in that case ?
Here is example how loading next route is delayed
https://codesandbox.io/s/vue-highcharts-demo-nn3uv
Related
I have simple routes: /follower/:token/edit and /follower/new
When I move from the first one to the second one by $router.push('/follower/new') befoureRouteEnter hook is triggered but callback function inside 'next' function does not (problem does not exist when I go from different routes or reload page).
beforeRouteEnter(to, from, next) {
debugger; //is triggered
next(vm => {
debugger; //is not triggered
})
}
Do you know what can be wrong?
Vue: 2.5.17
Vue-router: 3.0.1
Regards
If you are navigating between routes using the same component - vue tries to optimize by delivering a cashed version. I'm not sure if this is what you are experiencing – but you could try to force re-instantiation by adding a key value to your <router-view>.
A 'common' way to do this is to use $route.path
<router-view :key="$route.path"></router-view>
My Vue app shows a loading screen whenever a route is loading because code-split routes sometimes take a few seconds to load. It does this using a global beforeEach hook.
// App component
this.$router.beforeEach(() => { this.loading = true })
this.$router.afterEach(() => { this.loading = false })
this.$router.onError(() => { this.loading = false })
The problem with this is if I create a guard on a route definition (like beforeEnter), calling next(false) won't trigger afterEach or onError, so it leaves the app in a loading state forever.
One thing I tried was to use beforeResolve instead of beforeEach. This didn't work out because my beforeEnter route guards are asynchronous, and I want to show the loading screen while they execute.
Another possible solution is to always call next(error) instead of next(false) so that the onError hook is triggered. But that's a bug waiting to happen later on, when someone tries to use the navigation guard API as intended.
One last option is to isolate the this.loading logic into a globally-accessible class or event bus and make route guards handle it on a case-by-case basis.
But my preference would be to set it and forget it. Is there a global hook that gets called when navigation is cancelled? And why isn't afterEach called in this scenario?
How the route is changed, matters for my case.
So, I want to catch when the route is changed by a back button of browser or gsm.
This is what I have:
router.beforeEach((to, from, next) => {
if ( /* IsItABackButton && */ from.meta.someLogica) {
next(false)
return ''
}
next()
})
Is there some built-in solutions that I can use instead of IsItABackButton comment? Vue-router itself hasn't I guess but any workaround could also work here. Or would there be another way preferred to recognize it?
This is the only way that I've found:
We can listen for popstate, save it in a variable, and then check that variable
// This listener will execute before router.beforeEach only if registered
// before vue-router is registered with Vue.use(VueRouter)
window.popStateDetected = false
window.addEventListener('popstate', () => {
window.popStateDetected = true
})
router.beforeEach((to, from, next) => {
const IsItABackButton = window.popStateDetected
window.popStateDetected = false
if (IsItABackButton && from.meta.someLogica) {
next(false)
return ''
}
next()
})
Slight improvement to #yair-levy answer.
Wrapping push to own navigate method is not convenient because you usually want to call push() from various places. Instead, router original methods can be patched in one place without changes in remaining code.
Following code is my Nuxt plugin to prevent navigation triggered by back/forward buttons (used in Electron app to avoid back caused by mouse additional "back" button, which makes mess in Electron app)
Same principle can be used for vanilla Vue and to track common back button together with your custom handling.
export default ({ app }, inject) => {
// this is Nuxt stuff, in vanilla Vue use just your router intances
const { router } = app
let programmatic = false
;(['push', 'replace', 'go', 'back', 'forward']).forEach(methodName => {
const method = router[methodName]
router[methodName] = (...args) => {
programmatic = true
method.apply(router, args)
}
})
router.beforeEach((to, from, next) => {
// name is null for initial load or page reload
if (from.name === null || programmatic) {
// triggered bu router.push/go/... call
// route as usual
next()
} else {
// triggered by user back/forward
// do not route
next(false)
}
programmatic = false // clear flag
})
}
As stated by #Yuci, all the router hook callbacks are performed before popstate is updated (and therefore not helpful for this use case)
What you can do:
methods: {
navigate(location) {
this.internalNavigation = true;
this.$router.push(location, function () {
this.internalNavigation = false;
}.bind(this));
}
}
Wrap 'router.push' with you own 'navigate' function
Before calling router.push, set 'internalNavigation' flag to true
Use vue router 'oncomplete' callback to set internalNavigation flag back to false
Now you can check the flag from within beforeEach callback and handle it accordingly.
router.beforeEach((to, from, next) => {
if ( this.internalNavigation ) {
//Do your stufff
}
next()
})
I found a simple way to solve this after spending a lot of time trying to refine the codes to work well in my case and without a glitch.
export const handleBackButton = () => {
router.beforeEach((to, from, next) => {
if (window.event.type == 'popstate' && from.name == 'HomePage'){
next(false);
}else{
next();
}
});
}
The window.event.type == 'popstate' checks if the back button is pressed
And from.name == 'HomePage' checks the page on which the back button is pressed or you are routing from.
HomePage as the name where you want to disable back button. You can leave this condition if you want to disable it throughout the site.
next(false) and next() to stop or allow navigation respectively.
You can place the code in a navigationGuard.js file and import it to your main.js file
I tried other methods, including calling from the components but it produces a glitch and the rerouting becomes obvious. But this leaves no glitch at all.
Hope this works for you. Cheers
I had the same problem regarding detecting Back Button navigation as opposed to other types of navigation in my Vue App.
What I ended up doing was adding a hash to my real internal App navigation to differentiate between intended App navigation and Back Button navigation.
For example, on this route /page1 I want to catch Back Button navigations to close models that are open. Imagine I really wanted to navigate to another route, I'll add a hash to that route: /page2#force
beforeRouteLeave(to, from, next) {
// if no hash then handle back button
if (!to.hash) {
handleBackButton();
next(false); // this stops the navigation
return;
}
next(); // otherwise navigate
}
This is rather simplistic, but it works. You'll want to check what the hash actually contains if you use them for more than this in your app.
performance.navigation is deprecated so whatch out! https://developer.mozilla.org/en-US/docs/Web/API/Performance/navigation
When you want to register any global event listener you should be very careful with that. It will be called each time since registration moment untill you unregister that manualy. For me the case was that I have register popstate listener when component was created to listen and call some action when:
browser back button
alt + arrow back
back button in mouse
was clicked. After that I have unregister popstate listener to not call it in other components where I don't want it to be called, keep Your code and method calls clean :).
My code sample:
created() {
window.addEventListener('popstate', this.popstateEventAction );
},
methods: {
popstateEventAction() {
// ... some action triggered when the back button is clicked
this.removePopstateEventAction();
},
removePopstateEventAction() {
window.removeEventListener('popstate', this.popstateEventAction);
}
}
Best regards!
The accepted answer almost worked for me, but I found that the listener was behind by 1 click, probably due to the issue that #Yuci highlighted.
The answer from #farincz worked best for me, but since it wasn't written for vanilla Vue, I thought I'd write down what worked for me here:
// after createRouter
let programmatic = false;
(['push', 'replace', 'go', 'back', 'forward']).forEach(methodName => {
const method = router[methodName]
router[methodName] = (...args) => {
programmatic = true
method.apply(router, args)
}
})
router.beforeEach(async (to, from) => {
if(!from.name === null || !programmatic) {
// do stuff you want to do when hitting back/forward or reloading page
}
programmatic = false // clear flag
});
This is done very easily.
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
// History back position, if user click Back button
return savedPosition
} else {
// Scroll to top of page if the user didn't press the back button
return { x: 0, y: 0 }
}
}
})
Check here:
https://router.vuejs.org/guide/advanced/scroll-behavior.html#async-scrolling
I've got the following code:
const routes = [
{ path: '/', component: FooView },
{ path: '/bar', component: BarView }
];
const router = new VueRouter({
routes
});
router.beforeEach(function(to, from, next) {
if (to.path === '/bar') {
next('/');
}
next();
});
If I've omitted too much and you need to see other pieces of code related to the router let me know so I can fill it in.
If I open up a new tab and navigate to '/#/bar' I'm successfully redirected to '/#'. However, if I then go into the address bar and manually add '/#/bar' and hit enter I am not redirected. If I then hit enter in the address bar again I am redirected.
I've stepped through the code in the console and I see that it is calling next('/') and I see where it calls push('/') inside of next('/') but it doesn't take affect until I've hit enter in the address bar a second time.
I've tried using router.replace('/') but the behaviour is the same. I've tried using beforeEnter on the individual route but the behaviour is also the same.
Two links I've found where similar behaviour is discussed are: https://github.com/vuejs/vue-router/issues/748 and https://forum.vuejs.org/t/router-beforeeach-if-manually-input-adress-in-browser-it-does-not-work/12461/2 but neither helped me.
Is someone able to explain this? Is there a disconnect between what I'm trying to do and what functionality vue-router provides? If this behaviour isn't expected can someone propose a work around?
In the vue-router official documentation, the way you implement the beforeEach() is not recommended. Here is what the documentation says:
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. Here is an example of redirecting to user to /login if they are not authenticated:
// BAD
router.beforeEach((to, from, next) => {
if (!isAuthenticated) next('/login')
// if the user is not authenticated, `next` is called twice
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (!isAuthenticated) next('/login')
else next()
})
Not sure why the first one is a bad example, since both sample code should work exactly the same logically. My code pops error when the first time redirect the path using next('/'), however, the rerouting still success. Looking for answer from pros.
Without getting too excited (a lot of testing left to do) it appears as though I've managed to fix my issue.
Instead of:
router.beforeEach(function(to, from, next) {
if (to.path === '/bar') {
next('/');
}
next();
});
I changed the code to the following:
router.beforeEach(function(to, from, next) {
if (to.path === '/bar') {
next('/');
return;
}
next();
});
Note the added return; in the if statement.
I still have questions about the behaviour. In particular I need too investigate deeper why sometimes it would hit the route when the only difference is whether it is the first or second time I entered the URL into the address bar. I'm sure diving deeper into next will answer my question.
Anyway, adding the return; turned this into a non blocker.
As other answers mention, the next() function should be called exactly once inside the same guard.
It is important that it is called at least once or the guard will never complete and block the navigation.
As taken from the docs -->
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
The next() function signals that this guard has finished and the next guard may be called.
By doing this it is possible to create asynchronous guards that only finish when the next() function is called.
However the rest of the code inside the guard is still executed.
This means that if you call next() several times it will cause unpredictable behavior.
// This will work
if(condition){
next();
}
else{
next();
}
// This cause you pain!
next();
next();
The 'next' function also takes parameters:
// This aborts the current navigation and "stays" on the same route
next(false)
// These navigate to a different route than specified in the 'to' parameter
next('/');
next({path: '/'})
next({path: '/', query: {urlParam: 'value'}}) // or any option used in router.push -> https://router.vuejs.org/api/#router-forward
// If the argument passed to next is an instance of Error,
// the navigation will be aborted and the error
// will be passed to callbacks registered via router.onError().
next(new Error('message'))
i have a question regarding Javascript Promises in a VueJS setting, i have an application that uses an Action to either fetch a list of Countries from IndexedDB (if it's set ) or from an API by making an Axios HTTP Request.
Now, i'm returning a promise from the action because i want to be able to trigger some popups in the UI when this task is completed, and on top of that both Axios and Dexie(which im using for IndexedDB) run asyncronously through Promises themselves.
getCountries({commit, dispatch}) {
commit(types.MUTATIONS.SET_LOADING, true, {root: true})
commit(types.MUTATIONS.SET_LOADER_MESSAGE, "Loading Countries Data...", {root: true})
return new Promise((resolve, reject) => {
db.countries.count().then(value => {
if(value > 0) {
console.log("Loading Countries from IndexedDB")
db.countries.toArray().then(collection => {
commit(types.MUTATIONS.COUNTRIES.SET, collection, {root: true})
resolve(collection);
})
} else {
fetchCountriesData().then(data => {
console.log("Loading Countries from API Call")
commit(types.MUTATIONS.COUNTRIES.SET, data, {root: true})
db.countries.bulkAdd(data)
resolve(data)
}).catch(error => {
reject(error)
})
}
})
})
}
That is the code for the action, it simply does what i described above, the problem is that this results in an infinite loop where the LOADER Mutations get triggered over and over again.
Why exactly is this going on? Can anyone help me make sense of this? It seems that it runs the initial API action, but THEN after that, with the countries already loaded, it loops and runs again this time invoking the indexeddb mutation as well, which is strange, if i resolve it shouldn't it just end there?
EXTRA::
The action is invoked in a view that i have in my application, i invoke this in the created() hook so that i make sure that the list of countries is always loaded in my Vuex State.
created() {
this.getAllCountries().then(response => {}).catch(error => {
snackbar("Unable to load countries!", "error")
}).then(() => {
this.setLoadingStatus(false);
})
}
In this context, it does nothing if it's ok, but that might change in the future, on errors it should show a popup informing the users that the data could not be loaded, and in either case it should hide the loading bar (which is also handled through Vuex)
Could this be the cause of the problem?
Nevermind, i had a logic error in my code, basically in order to prevent anyone being able to click items while loading i set the view conditionally with a v-if="loading" so that if loading only show the Loader div and otherwise show the actual layout.
The problem with this approach is that it will re-trigger the created hook each time the main view is shown again, thus causing my silly loop.
Hope this helps someone in the future.