I have a (client-side) router in a Meteor app, and links using the {{pathFor}}
helper.
I am setting a dirty
flag in the Session
The new behavior for iron router should make this easier because it requires a call to this.next()
in the onBeforeAction hook (see iron router guide), so only call that when the session is not dirty or the user confirms the warning:
if(Session.get('dirty')) {
if(confirm("Are you sure you want to navigate away?")) {
this.next();
}
} else {
this.next();
}
The Iron Router API doesn't offer an easy way to achieve this. There is no way to cancel an ongoing transition from an onBeforeAction
hook. It has to be worked around by redirecting to the previous route.
/*
* Adds a confirmation dialogue when the current route contains unsaved changes.
*
* This is tricky because Iron Router doesn't support this out of the box, and
* the reactivity gets in the way.
* In this solution, redirecting to the current route is abused
* as a mechanism to stop the current transition, which Iron Router has no API
* for. Because the redirect would trigger the onStop hook, we keep track of
* whether to run the onStop hook or not ourselves in
* `skipConfirmationForNextTransition`.
*
* When `Session.get('formIsDirty')` returns `true`, the user will be asked
* whether he really wants to leave the route or not.
*
* Further, another confirmation is added in case the browser window is closed
* with unsaved data.
*
* This gist shows the basics of how to achieve a navigation confirmation,
* also known as canceling a route transition.
* This approach may fail if other route hooks trigger reruns of hooks reactively.
* Maybe setting `skipConfirmationForNextTransition` to `true` could help in those
* cases.
*/
Session.setDefault('formIsDirty', false)
const confirmationMessage = 'You have unsaved data. Are you sure you want to leave?'
// whether the user should confirm the navigation or not,
// set to `true` before redirecting programmatically to skip confirmation
let skipConfirmationForNextTransition = false
Router.onStop(function () {
// register dependencies immediately
const formIsDirty = Session.equals('formIsDirty', true)
// prevent duplicate execution of onStop route, because it would run again
// after the redirect
if (skipConfirmationForNextTransition) {
skipConfirmationForNextTransition = false
return
}
if (formIsDirty) {
const shouldLeave = confirm(confirmationMessage)
if (shouldLeave) {
Session.set('formIsDirty', false)
return
}
// obtain a non-reactive reference to the current route
let currentRoute
Tracker.nonreactive(function () {
currentRoute = Router.current()
})
skipConfirmationForNextTransition = true
// "cancel" the transition by redirecting to the same route
// this had to be used because Iron Router doesn't support cancling the
// current transition. `url` contains the query params and hash.
this.redirect(currentRoute.url)
return
}
})
// Bonus: confirm closing of browser window
window.addEventListener('beforeunload', event => {
if (Session.get('formIsDirty')) {
// cross-browser requries returnValue to be set, as well as an actual
// return value
event.returnValue = confirmationMessage // eslint-disable-line no-param-reassign
return confirmationMessage
}
})
An up-to-date version can be found in this gist.
From what I can tell this isn't possible with the iron-router API. What you could do however is override the Router.go method like so (somewhere in your client code):
var go = Router.go; // cache the original Router.go method
Router.go = function () {
if(Session.get('dirty')) {
if (confirm("Are you sure you want to navigate away?")) {
go.apply(this, arguments);
}
} else {
go.apply(this, arguments);
}
};
Is it somewhere specific you want to go? There is also Router.go(routeName) which will make the page point to the given routeName. What I was going for is maybe you can just force the Router to go to the current page hence neglecting the back action.
I found that rediecting in stop
works, and works even when you aren't changing routes via Router.go
(such as by links in my application).
Here is a coffeescript implementation using a class inherited from RouteController
class MyRouteController extends RouteController
stop: ->
# Save whether you data/form is dirty or whatever state you have in
# a Session variable.
if Session.get('formIsDirty')
if !confirm('You have unsaved data. Are you sure you want to leave?')
# Redirecting to the current route stops the current navigation.
# Although, it does rerun the route, so it isn't a perfect solution.
Router.go '/my_route'
# Return here so we don't perform any more of the stop operation.
return
# Otherwise do as normal.
super