When a user is about to leave the present route, I'd like to display a warning and let him choose between leaving (and losing changes) and staying in the current route.
In order to catch all possible transitions, I need to do this in the routes willTransition method.
I'm attempting to abort() and then retry() the transition if the user chooses to. But the retry doesn't seem to have any effect. It should be noted that it is called asynchronously. Here's a twiddle that demonstrates that: https://ember-twiddle.com/b6d8ddb665ff79f2988277912916e77b?openFiles=routes.my-route.js%2C
Here's my route example route:
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
willTransition(transition) {
transition.abort();
Ember.run.later(function(){
console.log('Transition... now!');
transition.retry();
}, 2000);
return true;
}
}
});
The log shows up, but I never get redirected to the application route.
Take a look into log. You will see that "Transition... now!" appears there every 2 sec. That shows that willTransition works again and again. So you need some flag that allows you to go away. Updated twiddle
When a user is about to leave the present route, I'd like to display a warning and let him choose between leaving (and losing changes) and staying in the current route.
For the above requirement, the below code is enough.
willTransition(transition) {
if (!confirm('Are you sure to navigate ?')) {
transition.abort();
return false;
}
return true;
}
Related
Context
I have this LightGallery plugin, and I noticed that when it is in fullscreen, clicking on the previous page button will not close the LightGallery but go "silently" to the previous page without the user to be aware of it.
In my case, I am building an SPA using VueJS and VueRouter, and when the user closes the LightGallery after clicking the previous page, it is confusing because the user expected to get back where he/she was.
I know that VueRouter has navigation guards, but I miss one feature, to easily know if the user is going back or not.
I am using VueRouter in "history" mode.
As I use the "history" mode, I guess this explains why the lg-hash plugin for LightGallery is of no help. It only appens a hash to my route, but the VueRouter is working independently of this hash.
Question
How to know if the user is going to the previous page using VueRouter?
In "history" mode, VueRouter is using the History API. Is there anyway to use it to have a clue? Or should I eventually store manually the history and process it at each route change?
I have found that we can listen to previous/next page using window.onpopstate = function() {}, but can I exclusively check for the previous page only using this method?
In my router instantation I have:
const router = new Router({
mode: "history",
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
to.meta.fromHistory = true;
return savedPosition;
} else {
return { x: 0, y: 0 };
}
},
// (...)
However, as you mentioned, there's no clear way to catch whether that's a back or a forward navigation taking place.
As per the official documentation:
The scrollBehavior function receives the to and from route
objects. The third argument, savedPosition, is only available if
this is a popstate navigation (triggered by the browser's
back/forward buttons).
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'))
In my index route, I have an observer that fires an action when the user object is set in the session service, as shown below. This works fine- the console logs 'index route observer fired' and the action fires.
routes/index
session: Ember.inject.service(),
sendTheAction: function() {
console.log('index route observer fired');
this.send('checkLicense');
}.observes('session.user'),
actions: {
checkLicense: function() {
if (this.get('session.user.email)) {
//get the user's email and send off an AJAX request.
}
},
}
I also have a logout route which (among other things) sets session.user to an empty object, and then transitions to the login route. I simply use a link-to helper pointing to 'logout' to initiate this.
routes/logout
session: Ember.inject.service(),
actions: {
didTransition: function() {
this.set('session.user', {});
this.transitionTo('login');
}
}
If I am in the index route and I click logout, the transition to logout begins. Then, when session.user is set to {} in the logout route's didTransition hook, the observer in the index route fires. The console logs 'index route observer fired' but then I get a console error saying
Error while processing route: logout Nothing handled the action 'checkLicense'.
I'm not sure how the observer in the previous route can still be fired by a change in the logout route's didTransition hook, as I thought didTransition only fired when the transition was fully resolved.
I think that it is then looking for the action "checkLicense" in the logout route.
Can anyone clarify why that observer still fires, and how to get around this?
The observer still fires and this is the expected behavior. The reason is; routes are SINGLETONs; they are not destroyed upon transitions. So; if you define an observer within a route and watch for values that might be updated by other routes, components, controllers, etc. then your observer will still work!!! So; be careful about using observers within singletons (controllers, services, routest, etc.).
I created a twiddle that shows the exact case you mentioned. The valid question here in fact is "why is the send action within index.js is delegated to the other route?" Here is the answer from the Ember.js api. It says "Sends an action to the router, which will delegate it to the currently active route..." in send method definition. So; the error occurs; because current active route is not index anymore; action is not found!
So; what can you do? Why are throwing an action to yourself within index.js? Just make a normal function call!!! If you still need the action; you can also call the same function from within the action. See the modified twiddle in order to see what I mean.
To sum up; observers within singletons will work no matter what if the dependent key(s) get(s) updated! Hope; it is all clear now. Best Regards.
So I have a setRouteleaveHook with function:
routerWillLeave(nextLocation) {
// return false to prevent a transition w/o prompting the user,
// or return a string to allow the user to decide:
console.log("prompt: ", this.props.prompt);
if (this.props.prompt) {
return "Are you sure you wish to navigate? Unsaved data will be lost. Try to use the 'next' button instead if moving to the next part.";
} else {
return true;
}
}
this.props.prompt is a variable in the store. It seems that when I update it in it's parent, the prompt doesn't get altered until the next time I visit it.
Is there a better way to trigger these prompts? (Given that it is the parent component that is doing the router.push)
import {withRouter} from 'react-router';
export default withRouter(connect(YourSelectors,YourActions)(YourPage))
I have an long operation on the page. How can I said user wait a bit when he make the navigation change?
The idea is to show the promt dialog. But the trouble prevent default Aurelia router behavior.
How make it works in Aurelia.js?
You could use the canDeactivate hook of your view-model. Like this:
canDeactivate() {
return confirm('Are you sure you want to leave this page?');
}
canDeactivate() - Implement this hook if you want to control whether or not the router can navigate away from your view-model when moving to a new route. Return a boolean value, a promise for a boolean value, or a navigation command.
More information at http://aurelia.io/docs.html#/aurelia/framework/1.0.0-beta.1.2.2/doc/article/cheat-sheet/7