Flawless Ruby

Brian Del Vecchio's Imperfect Code Blog

Conditional Refresh on Browser Back

Yesterday one of our contract QA staff filed this bug description:

After you clone a report, navigate to to a different page. Then hit the browser’s back button notice the cloned report is not present.

My first thought was, that’s how the back button works–there’s nothing I can do about it. But then I thought about it for a bit. This is a basic CRUD #index page, but because we’re doing this #clone operation via AJAX, we’ve invalidated the page as cached by the browser. We also do this with #delete–perform the operation via AJAX, and update the page content via DOM manipulation.

I looked around for a way to tell the browser to explicitly invalidate the current page, so that it would be reloaded in case the user navigated to the page with the Back button. But to no avail: there’s no cross-browser way to do that.

But while reviewing the specs for window.location at MDN, I thought, maybe we can use window.location.replace() to set a ‘cache invalid’ flag in the URL that we could test for on page load. If the flag is set, we can force a reload of the page with window.location.reload(force).

Here’s the modified AJAX handler, which replaces the current browser history entry “/reports” with “/reports#modified”.

conditional refresh on browser back
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // generic AJAX clone button
  $(".actions a.clone").on('click', function(event){
    event.preventDefault();

    // [edited for brevity]

    $.ajax({url: $self.attr('href'), type: 'POST', cache: false,
      dataType: 'html',
      success: function(data){

        $(data).insertAfter($row); // insert the newly created row

        // mark this page as modified by AJAX
        window.location.replace(window.location.href + '#modified');

      }
      // [edited for brevity]
    });

    return false;

  });

Now this test is executed at script load time:

conditional refresh handler
1
2
3
4
5
6
  // if the hash is 'modified', then it's a cached page which was invalidated by AJAX.
  // so reload this page now.

  if (window.location.hash === '#modified'){
    window.location.assign(window.location.pathname + window.location.search); // yields href with no hash
  }

Note that the reload test is executed directly, not in the document ready event handler. If we know this page is going to be reloaded, we should do it as soon as possible, so don’t let the page finish rendering first, then reload it.

I also looked at HTML5 History, but I think that if I can accomplish this strictly using the older API, I’m happy. This is not really an ideal use case for HTML5 History, anyway. The problem that HTML5 History was designed to solve is when your complex interactions can insert or otherwise manipulate the browser’s history, as seen primarily by the Back button. If you’re building a multi-step wizard, and you’re changing the page content, a user might expect each step to appear in the browser history, and expect the Back button to take them to the previous step, not the previous full page that was loaded from the server. So HTML5 has lots of support for this, and current Chrome and Firefox fully support them. For older browsers there’s an excellent polyfill from Ben Lupton called history.js.

More detail: HTML5 History at MDN When Can I Use?

A great example of this type of functionality is GitHub’s project navigation. As you navigate through a tree, the content is replaced via AJAX, but the back button works just as you’d expect. This was written by Chris Wanstrath and published as pjax.

Comments