Search code examples
backbone.jsbrowser-historypushstatebackbone-routing

Preventing page navigation inside a Backbone-driven SPA


The justification

In my BB app, I allow rapid input from users which gets queued & sent off periodically in the background to the server. The problem I currently have is if a user leaves the page they effectively discard any pending changes sitting in the queue.

So basically what I want to do is inform the user before they leave to give them the opportunity to wait for the changes to be saved rather than just exiting & discarding.

The nitty gritty

So for the general cases where the user refreshes or attempts to navigate to an external URL we can handle the onbeforeunload event. Where it becomes slightly tricky is when we are in the context of an SPA whereby switching between pages does not cause a page refresh.

My immediate thought was to use a global click event handler for all anchors and validate whether or not I want to allow the click, which would work for in-site link navigation. However, where this falls over is navigating via the browsers Back/Forward buttons.

I also had a look at Backbone.routefilter, which at first glance appeared to do exactly what I needed. However, using the simple case as described in the docs, the route was still being executed.

The question

How do we intercept navigation for all scenarios within a Backbone SPA?


Solution

  • Direct link navigation

    Use a global event handler to capture all click events

    $(document).on('click', 'a[href^="/"]', function (e) {
        var href = $(e.currentTarget).attr('href');
        e.preventDefault();
        if (doSomeValidation()) {
            router.navigate(href, { trigger: true });
        }
    });
    

    Page refreshing / external URL navigation

    Handle the onbeforeunload event on the window

    $(window).on('beforeunload', function (e) {
        if (!doSomeValidation()) {
            return 'Leaving now will may result in data loss';
        }
    });
    

    Browser back/forward button navigation

    Behind the scenes Backbone.Router uses the Backbone.history which ultimately leverages the HTML5 pushstate API. Depending on what options you pass to Backbone.history.start, and what your browser is capable of, the API will hook into either the onhashchange event or the onpopstate event.

    Delving into the source for Backbone.history.start it becomes apparent that regardless of whether you are using push state or not, the same event handler is used i.e. checkUrl.

    if (this._hasPushState) {
        addEventListener('popstate', this.checkUrl, false);
    } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
        addEventListener('hashchange', this.checkUrl, false);
    } else if (this._wantsHashChange) {
        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
    }
    

    Therefore, we can override this method & perform our validation in there

    var originalCheckUrl = Backbone.history.checkUrl;
    Backbone.history.checkUrl = function (e) {
        if (doSomeValidation()) {
            return originalCheckUrl.call(this, e);
        } else {
            // re-push the current page into the history (at this stage it's been popped)
            window.history.pushState({}, document.title, Backbone.history.fragment);
            // cancel the original event
            return false;
        }
    };