Prevent navigating to another view if contents are

2020-05-26 11:45发布

We have a backbone.js app that displays a number of forms to the user. What we want is very simple: if the user goes to another page without saving the filled-in form, we want to display a confirmation dialog.

In classic forms this is easy enough, just implement window.onbeforeunload (or $(window).on('beforeunload') in jQuerysh). But backbone apps only have one view, typically. I tried a bit using onHashChange, but returning false in that callback does not prevent Backbone from still going to the other view.

Pointers are appreciated. Searching the interwebs didn't find me any valid response.

6条回答
狗以群分
2楼-- · 2020-05-26 12:23

I've been dealing with this issue for a little while, and I've come up with a solution. I based my solution off this example.

The idea is to override the navigate method, and use jQuery deferred objects to wait to the appropriate time to navigate. In my case, if a user tried to navigate from my view that was dirty, a dialog needed to show that asked the user if:

1) Save the changes, then navigate 2) Don't save the changes, and navigate 3) Cancel the navigation and remain on the existing page

Below is my code for the navigate method in the Router:

navigate: function(fragment, trigger) {
    var answer,
          _this = this;

    answer = $.Deferred();
    answer.promise().then(function() {
        return Backbone.Router.prototype.navigate(fragment, trigger);
    });

    if(fragment !== undefined){     
        var splitRoute = fragment.split('/');
        app.currentPatronSection = splitRoute[splitRoute.length - 1];
    }

    if (app.recordChanged) {
        this.showConfirm(function(ans){
            // Clear out the currentView
            app.currentView = undefined;
            answer.resolve();
        }, function(){

        });
        return answer.promise();
    } else {
        return answer.resolve();
    }
    return Backbone.Router.prototype.navigate(fragment, trigger);

},

The showConfirm method presents the dialog with the three options I listed above. Based on the user's choice, I save the form, then resolve the answer to navigate, etc.

查看更多
▲ chillily
3楼-- · 2020-05-26 12:24

I think you could hack Backbone.history.loadUrl ( http://documentcloud.github.com/backbone/docs/backbone.html#section-137 ). I did a quick test, this code does a check every time you change pages. You'll want to add code to activate the check only when there is actually a reason for it.

var goingBack = false;
function doCheck() {
  // TODO: add code that checks the app state that we have unsaved data
  return goingBack || window.confirm("Are you sure you want to change pages?");
}

var oldLoad = Backbone.History.prototype.loadUrl;
Backbone.History.prototype.loadUrl = function() {
  if(doCheck()) {
    return oldLoad.apply(this, arguments);
  } else {
    // change hash back
    goingBack = true;
    history.back();
    goingBack = false;
    return true;
  }
}

You'll have to handle window.onbeforeunload as well, because the user might still leave the page entirely.

查看更多
够拽才男人
4楼-- · 2020-05-26 12:29

Since version 1.2.0 you can override method Router.execute and return false to cancel routing, like this:

execute: function(callback, args, name) {
    if (!changesAreSaved) {
        // tip: .confirm returns false if "cancel" pressed
        return window.confirm("You sure have some unsaved "
          + "work here, you want to abandon it?");
    }

    // this is the default part of "execute" - running the router action
    if (callback)
        callback.apply(this, args);
}
查看更多
地球回转人心会变
5楼-- · 2020-05-26 12:33

I was getting multiple calls to loadUrl per reroute using Dénes's solution, so I decided to try this method, which worked for me.

/**
 * Monkey patches Backbone to prevent a reroute under certain conditions.
 *
 * Solution inspired by: https://stackoverflow.com/a/24535463/317135
 *
 * @param Backbone {Backbone}
 *   Backbone 1.0.0 reference
 * @param disallowRouting {function(): boolean}
 *   Function returning `true` when routing should be disallowed.
 */
export default function permitRouteWhen(Backbone, permitRouting) {
  if (Backbone.VERSION !== '1.0.0') {
    console.error(
      `WARNING: Expected to be hacking Backbone version 1.0.0, but got
      ${Backbone.VERSION} - this could fail.`
    );
  }

  const { checkUrl } = Backbone.history;

  Backbone.history.checkUrl = function(event) {
    if (!permitRouting()) {
      event.preventDefault();
      return;
    }
    return checkUrl.apply(this, arguments);
  }
}

Use it like this:

import permitRouteWhen from './backbone-permit-route-hack';
permitRouteWhen(window.Backbone, () => confirm('you wanna route?'));
查看更多
太酷不给撩
6楼-- · 2020-05-26 12:34

I would avoid hacking around with Backbone. You could do this globally for all links by replacing the part where you would normally start Backbone.history with something like

initRouter: function () {
    Backbone.history.start({ pushState: true });
    $(document).on('click', 'a', function (ev) {
        var href = $(this).attr('href');
        ev.preventDefault();
        if (changesAreSaved) {
            router.navigate(href, true);
        }
    });
}

You need of course to replace the changesAreSaved with something that makes sense and add whatever other login you have about handling links.

查看更多
家丑人穷心不美
7楼-- · 2020-05-26 12:34

I would also hack Backbone.history.loadUrl, that's where loading the route callbacks happen.

// ALLOW PREVENTING HASH NAVIGATION

var originalFn = Backbone.history.loadUrl;

Backbone.history.loadUrl = function() {
    // I introduced an application state variable, but it can be solved in multiple ways
    if (app && app.states.isNavigationBlocked) {
        var previousFragment = Backbone.history.fragment;
        window.location.hash = '#' + previousFragment;
        return false;
    }
    else {
        return originalFn.apply(this, arguments);
    }
};

Explanation:

1)

Backbone listens to the hashchange event and sets Backbone.history.checkUrl as a callback: https://github.com/jashkenas/backbone/blob/1.1.2/backbone.js#L1414

Backbone.$(window).on('hashchange', this.checkUrl);

2)

Backbone.history.checkUrl checks if the hash has changed and calls Backbone.history.loadUrl

checkUrl: function(e) {
  var current = this.getFragment();
  if (current === this.fragment && this.iframe) {
    current = this.getFragment(this.getHash(this.iframe));
  }
  if (current === this.fragment) return false;
  if (this.iframe) this.navigate(current);
  this.loadUrl();
},

3)

Backbone.history.loadUrl finds the first matching route and calls its callback:

loadUrl: function(fragment) {
  fragment = this.fragment = this.getFragment(fragment);
  return _.any(this.handlers, function(handler) {
    if (handler.route.test(fragment)) {
      handler.callback(fragment);
      return true;
    }
  });
},

Useful note:

Backbone.history.fragment stores the current hash, it's set in Backbone.history.loadUrl, so we can access it AFTER the hashchange event but BEFORE the router callbacks do their job.

查看更多
登录 后发表回答