Intercepting call to the back button in my AJAX application

JavascriptAjaxCross BrowserBrowser History

Javascript Problem Overview


I have an AJAX app. A user clicks a button, and the page's display changes. They click the back button, expecting to go to the original state, but instead, they go to the previous page in their browser.

How can I intercept and re-assign the back button event? I've looked into libraries like RSH (which I couldn't get to work...), and I've heard that using the hash tag somehow helps, but I can't make sense of it.

Javascript Solutions


Solution 1 - Javascript

Ah, the back button. You might imagine "back" fires a JavaScript event which you could simply cancel like so:

// this does not work
document.onHistoryGo = function() { return false; }

No so. There simply is no such event.

If you really do have a web app (as opposed to just a web site with some ajaxy features) it's reasonable to take over the back button (with fragments on the URL, as you mention). Gmail does this. I'm talking about when your web app in all in one page, all self-contained.

The technique is simple — whenever the user takes action that modifies things, redirect to the same URL you're already on, but with a different hash fragment. E.g.

window.location.hash = "#deleted_something";
...
window.location.hash = "#did_something_else";

If the overall state of your web app is hashable, this is a great place to use a hash. Say you have a list of emails, maybe you'd concatenate all their IDs and read/unread statuses, and take an MD5 hash, using that as your fragment identifier.

This kind of redirect (hash only) doesn't try to fetch anything from the server, but it does insert a slot in the browser's history list. So in the example above, user hits "back" and they're now showing #deleted_something in the address bar. They hit back again and they're still on your page but with no hash. Then back again and they actually go back, to wherever they came from.

Now the hard part though, having your JavaScript detect when the user hit back (so you can revert state). All you do is watch the window location and see when it changes. With polling. (I know, yuck, polling. Well, there's nothing better cross-browser right now). You won't be able to tell if they went forward or back though, so you'll have to get creative with your hash identifiers. (Perhaps a hash concatenated with a sequence number...)

This is the gist of the code. (This is also how the jQuery History plugin works.)

var hash = window.location.hash;
setInterval(function(){
    if (window.location.hash != hash) {
        hash = window.location.hash;
        alert("User went back or forward to application state represented by " + hash);
    }
}, 100);

Solution 2 - Javascript

To give an up-to-date answer to an old (but popular) question:

> HTML5 introduced the history.pushState() and history.replaceState() methods, which allow you to add and modify history entries, respectively. These methods work in conjunction with the window.onpopstate event. > > Using history.pushState() changes the referrer that gets used in the HTTP header for XMLHttpRequest objects created after you change the state. The referrer will be the URL of the document whose window is this at the time of creation of the XMLHttpRequest object.

Source: Manipulating the browser history from Mozilla Developer Network.

Solution 3 - Javascript

Using jQuery I've made a simple solution:

$(window).on('hashchange', function() {
    top.location = '#main';
	// Eventually alert the user describing what happened
});

So far only tested in Google Chrome though.

This solved the problem for my web app which is also highly AJAX-based.

It is perhaps a little hack'ish - but I'd call it elegant hacking ;-) Whenever you try to navigate backwards it pops a hash part in the URI which is technically what it then tries to navigate backwards over.

It intercepts both clicking the browser button and mouse button. And you can't bruteforce it backwards either by clicking several times a second, which is a problem that would occur in solutions based on setTimeout or setInterval.

Solution 4 - Javascript

I really appreciate the explanation given in darkporter's answer, but I think it can be improved by using a "hashchange" event. As darkporter explained, you want to make sure that all of your buttons change the window.location.hash value.

  • One way is to use <button> elements, and then attach events to them that then set window.location.hash = "#!somehashstring";.
  • Another way to do this is to just use links for your buttons, like <a href="#!somehashstring">Button 1</a>. The hash is automatically updated when these links are clicked.

The reason I have an exclamation mark after the hash sign is to satisfy Google's "hashbang" paradigm (read more about that), which is useful if you want to be indexed by search engines. Your hashstring will typically be name/value pairs like #!color=blue&shape=triangle or a list like #!/blue/triangle -- whatever makes sense for your web app.

Then you only need to add this bit of code which will fire whenever the hash value changes (including when the back button is hit). No polling loop seems to be necessary.

window.addEventListener("hashchange", function(){
    console.log("Hash changed to", window.location.hash);
    // .... Do your thing here...
});

I haven't tested in anything but Chrome 36, but according to caniuse.com, this should be available in IE8+, FF21+, Chrome21+, and most other browsers except Opera Mini.

Solution 5 - Javascript

It is very easy toi disable the browser BACK button, like the JQuery code below:

// History API 
if( window.history && window.history.pushState ){

  history.pushState( "nohb", null, "" );
  $(window).on( "popstate", function(event){
    if( !event.originalEvent.state ){
      history.pushState( "nohb", null, "" );
      return;
    }
  });
}

You can see it working and more examples here dotnsf and here thecssninja

Thanks !

Solution 6 - Javascript

I worked out a way to override normal history behavior and create distinct back and forward button events, using the HTML5 history API (it won't work in IE 9). This is very hacky, but effective if you wanted to intercept the back and forward button events and handle them however you want. This could be useful in a number of scenarios, e.g. if you were displaying a remote desktop window and needed to reproduce back and forward button clicks on the remote machine.

The following would override the back and forward button behavior:

var myHistoryOverride = new HistoryButtonOverride(function()
{
    console.log("Back Button Pressed");
    return true;
},
function()
{
    console.log("Forward Button Pressed");
    return true;
});

If you returned false in either of those callback functions, you would be allowing the browser to proceed with a normal back/forward operation and leave your page.

Here is the full script required:

function addEvent(el, eventType, handler) {
    if (el.addEventListener) { // DOM Level 2 browsers
       el.addEventListener(eventType, handler, false);
    } else if (el.attachEvent) { // IE <= 8
        el.attachEvent('on' + eventType, handler);
    } else { // ancient browsers
        el['on' + eventType] = handler;
    }
}
function HistoryButtonOverride(BackButtonPressed, ForwardButtonPressed)
{
	var Reset = function ()
	{
		if (history.state == null)
			return;
		if (history.state.customHistoryStage == 1)
			history.forward();
		else if (history.state.customHistoryStage == 3)
			history.back();
	}
	var BuildURLWithHash = function ()
	{
		// The URLs of our 3 history states must have hash strings in them so that back and forward events never cause a page reload.
		return location.origin + location.pathname + location.search + (location.hash && location.hash.length > 1 ? location.hash : "#");
	}
	if (history.state == null)
	{
		// This is the first page load. Inject new history states to help identify back/forward button presses.
		var initialHistoryLength = history.length;
		history.replaceState({ customHistoryStage: 1, initialHistoryLength: initialHistoryLength }, "", BuildURLWithHash());
		history.pushState({ customHistoryStage: 2, initialHistoryLength: initialHistoryLength }, "", BuildURLWithHash());
		history.pushState({ customHistoryStage: 3, initialHistoryLength: initialHistoryLength }, "", BuildURLWithHash());
		history.back();
	}
	else if (history.state.customHistoryStage == 1)
		history.forward();
	else if (history.state.customHistoryStage == 3)
		history.back();

	addEvent(window,"popstate",function ()
	{
		// Called when history navigation occurs.
		if (history.state == null)
			return;
		if (history.state.customHistoryStage == 1)
		{
			if (typeof BackButtonPressed == "function" && BackButtonPressed())
			{
				Reset();
				return;
			}
			if (history.state.initialHistoryLength > 1)
				history.back(); // There is back-history to go to.
			else
				history.forward(); // No back-history to go to, so undo the back operation.
		}
		else if (history.state.customHistoryStage == 3)
		{
			if (typeof ForwardButtonPressed == "function" && ForwardButtonPressed())
			{
				Reset();
				return;
			}
			if (history.length > history.state.initialHistoryLength + 2)
				history.forward(); // There is forward-history to go to.
			else
				history.back(); // No forward-history to go to, so undo the forward operation.
		}
	});
};

This works by a simple concept. When our page loads, we create 3 distinct history states (numbered 1, 2, and 3) and navigate the browser to state number 2. Because state 2 is in the middle, the next history navigation event will put us in either state 1 or 3, and from this we can determine which direction the user pressed. And just like that, we've intercepted a back or forward button event. We then handle it however we want and return to state number 2 so we can capture the next history navigation event.

Obviously, you would need to refrain from using history.replaceState and history.pushState methods while using the HistoryButtonOverride script, or else you'd break it.

Solution 7 - Javascript

Due to privacy concerns, it's impossible to disable the back button or examine the user's history, but it is possible to create new entries in this history without changing pages. Whenever your AJAX application changes state, update top.location with a new URI fragment.

top.location = "#new-application-state";

This will create a new entry in the browser's history stack. A number of AJAX libraries already handle all the boring details, such as Really Simple History.

Solution 8 - Javascript

I do something like this. I keep an array with previous app states.

Initiated as:

var backstack = [];

Then I listen for changes in the location hash, and when it changes I do this:

if (backstack.length > 1 && backstack[backstack.length - 2] == newHash) {
    // Back button was probably pressed
    backstack.pop();
} else
    backstack.push(newHash);

This way I have a somewhat simple way of keeping track of user history. Now if I want to implement a back button in the app (not the browser-botton), I just make it do:

window.location.hash = backstack.pop();

Solution 9 - Javascript

In my situation, I wanted to prevent the back button from sending the user to the last page and instead, take a different action. Using the hashchange event I have come up with a solution that worked for me. This script assumes the page you are using it on doesn't already use a hash, as is in my case:

var hashChangedCount = -2;
$(window).on("hashchange", function () {
    hashChangedCount++;
    if (top.location.hash == "#main" && hashChangedCount > 0) {
        console.log("Ah ah ah! You didn't say the magic word!");
    }
    top.location.hash = '#main';
});
top.location.hash = "#";

The problem I was having with some of the other answers is the hashchange event would not fire. Depending on your situation, this may work for you, too.

Solution 10 - Javascript

The back button in the browsers normally tries to go back to the previous address. there is no native device back button pressed event in browser javascript, but you can hack it with playing with location and hash to change your app state.

imagine the simple app with two views:

  • default view with a fetch button
  • result view

to implement it, follow this example code:

function goToView (viewName) {
  // before you want to change the view, you have to change window.location.hash.
  // it allows you to go back to the previous view with the back button.
  // so use this function to change your view instead of directly do the job.
  // page parameter is your key to understand what view must be load after this.

  window.location.hash = page
}

function loadView (viewName) {
    // change dom here based on viewName, for example:

    switch (viewName) {
      case 'index':
        document.getElementById('result').style.display = 'none'
        document.getElementById('index').style.display = 'block'
        break
      case 'result':
        document.getElementById('index').style.display = 'none'
        document.getElementById('result').style.display = 'block'
        break
      default:
        document.write('404')
    }
}

window.addEventListener('hashchange', (event) => {
  // oh, the hash is changed! it means we should do our job.
  const viewName = window.location.hash.replace('#', '') || 'index'

  // load that view
  loadView(viewName)
})


// load requested view at start.
if (!window.location.hash) {
  // go to default view if there is no request.
  goToView('index')
} else {
  // load requested view.
  loadView(window.location.hash.replace('#', ''))
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionZack BurtView Question on Stackoverflow
Solution 1 - JavascriptjpsimonsView Answer on Stackoverflow
Solution 2 - JavascriptgitaarikView Answer on Stackoverflow
Solution 3 - JavascriptJensView Answer on Stackoverflow
Solution 4 - JavascriptLukeView Answer on Stackoverflow
Solution 5 - JavascriptRoberval Sena 山本View Answer on Stackoverflow
Solution 6 - JavascriptBrianView Answer on Stackoverflow
Solution 7 - JavascriptMatthewView Answer on Stackoverflow
Solution 8 - JavascriptTrenskowView Answer on Stackoverflow
Solution 9 - JavascriptSwisher SweetView Answer on Stackoverflow
Solution 10 - JavascriptNainemomView Answer on Stackoverflow