Aborting navigation with Meteor iron-router
Asked Answered
G

5

7

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 when the user changes a form field, and I want to trigger a warning and allow the user to stop navigating away from the page if the flag is set, basically like an onunload handler.

I've tried to do this with:

Router.onBeforeAction(function(pause) {
    var self = this;

    if (!this.ready()) {
        return;
    }

    if(Session.get('dirty')) {
        if(!confirm("Are you sure you want to navigate away?")) {
            pause();
        }
    }
});

However, whilst I get the prompt, I'm still being navigated away. That is, the pause() doesn't seem to stop the subsequent router action, whatever it is.

What am I doing wrong?

Grazier answered 23/6, 2014 at 14:0 Comment(0)
S
6

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);
  }
};
Selenodont answered 24/6, 2014 at 2:54 Comment(1)
You sir, are a genius. :) It would be great if there was a slightly less hacky way to do this, but this definitely works.Grazier
S
0

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.

Stacey answered 23/6, 2014 at 15:8 Comment(1)
I want the user not to leave the page, i.e. I want to abort the route.Grazier
M
0

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();
}
Miceli answered 14/12, 2014 at 3:10 Comment(0)
O
0

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
Overthecounter answered 30/1, 2015 at 13:37 Comment(0)
L
0

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.

Lannielanning answered 7/3, 2016 at 22:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.