Detect Back Button in Navigation Guards of Vue-Router - javascript

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

Related

Is there a way to make protected route in sapui5

getRouter: function () {
return UIComponent.getRouterFor(this);
},
onInit: function () {
firebase.auth().onAuthStateChanged((user) => {
if (!user) {
this.getRouter().navTo("login");
}
});
}
Is there a way to make my routes protected, it is no problem when the first time I type in browser, I got redirected, but if I logout from the component, and manually type in browser /admin, I am not redirected, so onInit works only one time. I am new to sapui5, and in React it is different. Can someone please help and explain if there is a way to make some kind of protection on routes. Thanks.
The correct event to listen for is patternMatched
This is an example of how to protect a single route called "myProtectedRoute":
onInit: function () {
this.getRouter().getRoute("myProtectedRoute").attachPatternMatched((event) => {
if (user.isAuthenticated) {
// load and show data
} else {
this.getRouter().navTo("login");
}
})
}
If you wish to be notified when any route is matched you can read more in this tutorial https://openui5.hana.ondemand.com/topic/4a063b8250f24d0cbf7c689821df7199

Unsaved changes, redirect prompt before user gets redirected

In one of my React projects I have a simple form that tracks changes. I don't want that the user simply can leave the current page when unsaved changes would get lost.
If the user wants to navigate away from the page, he should get informed by a popup, that by proceeding changes are lost.
I thought to tackle this problem by using a custom hook that tracks outside clicks of my component. If a link is being clicked (outside of my ref), the redirection should be postponed, maybe until the user confirms at the popup that changes can be indeed lost. Then, a redirection should occur.
My custom hook looks like that
const useLeaveHook = (ref) => {
useEffect(() => {
function handleLeaveFunction(event) {
const handleLinkClickBehaviour = (target) => {
// modify the click behavior. If we are on the page click listeners are not allowed
target.onclick=(e) => {
e.preventDefault();
}
}
if (ref.current && !ref.current.contains(event.target)) {
let target = event.target;
// look if parent is a anchor element. If so, call the handleFunction.
// Then, the onclick handler will be modified via handleLinkClickBehaviour.
while (target && target.nodeName !== 'A') {
target = target.parentNode
}
if (target) {
handleLinkClickBehaviour(target);
}
}
}
document.addEventListener("mousedown", handleLeaveFunction);
return () => {
document.removeEventListener("mousedown", handleLeaveFunction);
};
}, [ref]);
}
The problem is now: I work with 'react-router-dom' and I don't wan't to manipulate my onclick listeners, that's a no go. (Thus, on page leave, I could revert the click behavior)
Also, I somehow have to store the redirection state after the user executes the popup, such that the user will be redirected to the desired route.
I have no idea how I could do this. It is important that this is a solution that works with react-router-dom.

How do I add a body class when a Vue Component is in the viewport?

Is there a way that I can add a class, maybe to the body tag, when a user clicks on a router link. Then remove this class again when they leave the view?
The reason being is that for the app shell I have a fixed header & footer. All views (Vue Components) feature these two elements as part of the design, except just one page where these two elements need removing.
I figured that if I could add a body class dynamically, I could then add the below CSS. Can this be done?
.minimal-shell .header,
.minimal-shell .footer{
display:none;
}
There are several ways to do what you are asking, as you can imagine.
1. Vue-Router Hooks
When you navigate to route components (components you render inside <router-view>) these components will have special router lifecycle-hooks you can use:
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// when route changes and this component is already rendered,
// the logic will be slightly different.
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
More about this in the official documentation
2. Vue-Router navigation guards
You could also use navigation guards - a function that is called at a certain point during a route navigation:
router.beforeEach((to, from, next) => {
// ...
})
More about this also in the official documentation
3. Explicit, component-specific action
Of course, you can also trigger all changes you want in the components itself. Either in the components you navigate to - or a root component for your <router-view>. My first idea would be to handle the logic in a watcher:
watch: {
$route: {
immediate: true, // also trigger handler on initial value
handler(newRoute) {
if (newRoute.params.myParam === 'my-param') {
document.body.classList.add('my-class');
}
}
}
}
I personally would watch the route in a root layout-component as I like to keep naviation guards for business logic / authentication (not style).
I'm on mobile so I apologize for formatting. You can just provide a class based on a prop.
<body class="[isInView ? 'view-class' : '']">
<Linker #click="goToView" />
</body>
Model:
data: function() {
return {
isInView: false;
}
},
methods: {
... ,
goToView() {
this.isInView = true;
this.$router.push({ path: 'view' });
}
}

vue-router's beforeEach guard exhibiting weird behaviour occasionally

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'))

Check permissions before vue.js route loads

does anyone know how to check a user's permissions before a vue.js route is rendered? I came up with a partial solution by checking permissions in the created stage of a component:
created: function () {
var self = this;
checkPermissions(function (result) {
if(result === 'allowed') {
// start making AJAX requests to return data
console.log('permission allowed!');
} else {
console.log('permission denied!');
self.$router.go('/denied');
}
});
}
However, the issue is the entire page loads momentarily (albeit without any data) before the checkPermission() function gets activated and re-routes to /denied.
I also tried adding the same code in the beforeCreate() hook, but it didnt seem to have any effect.
Does anyone else have any other ideas? Note - the permissions vary from page to page.
Thanks in advance!
The thing you need is defined in Vue Router Docs as Data Fetching - https://router.vuejs.org/en/advanced/data-fetching.html
So, as docs says, sometimes you need to fetch data when route is activated.
Keep checkPermissions in created hook, and then watch the route object:
watch: {
// call again the method if the route changes
'$route': 'checkPermissions'
}
A maybe more handy solution would be move logic from created hook into separated method and then call it in hook and in watch object too, as well.(Using ES6 syntax bellow)
export default {
created() {
this.checkPermissions()
},
watch: {
'$route': 'checkPermissions'
},
methods: {
checkPermissions() {
// logic here
}
}
}

Categories

Resources