Aborting navigation with Meteor iron-router

前端 未结 5 1215
遥遥无期
遥遥无期 2021-01-12 08:54

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

相关标签:
5条回答
  • 2021-01-12 09:22

    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();
    }
    
    0 讨论(0)
  • 2021-01-12 09:33

    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.

    0 讨论(0)
  • 2021-01-12 09:36

    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);
      }
    };
    
    0 讨论(0)
  • 2021-01-12 09:39

    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.

    0 讨论(0)
  • 2021-01-12 09:39

    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
    
    0 讨论(0)
提交回复
热议问题