6

I have to maintain a site with an AJAX search form (for lets say books) and I want the search results to be a part of the browser history.

So I set the "popstate" event handler

window.addEventListener('popstate', function(e) {
    alert("popstate: " + JSON.stringify(e.state));
});

and call pushState() upon the successfull AJAX request:

var stateObj = { q: "harry potter" };
window.history.pushState(stateObj, "Result", "/search");

Then my clickflow is:

  1. Load the page with the search form - NORMAL request - path is /
  2. Enter a query string and submit the form - AJAX request - path changes to /search
  3. Click on a search result - NORMAL request - path changes to /books/harry-potter

  1. When I now go back by clicking the browser back button I would expect the "popstate" event to be triggered with the according state object. But nothing happens. - path changes to /search

  1. When I then go back one more time I get an alert with popstate: null - path changes to /

  2. And when I go forward after that, I get popstate: {"q":"harry potter"} - path changes to /search


So, the important popstate event (the one with the query string) gets only triggered when going forward, but not when going back.

Is this because im navigating back from a site, whose history entry has been created automatically instead of programmatically by pushState? But if so, the History API would only make sense for complete single page applications.

Behaviour is the same in any browser. Hope you can help me out :)

jarvisccc
  • 61
  • 1
  • 3
  • Popstate only fires whenever the URL changes but you are still on the same page. Thus, when you press the back button from /books/harry-potter/, you simply load /search/ and no popstate event fires. So, a work-around is: on page load of /search/ you are going to have to read the URL and extract the search query. – Darryl Huffman Feb 21 '18 at 18:29
  • Thanks for your answer, but what do you mean with "only when the URL changes"? The URL is changing all the time (from / to /search, from /search to /books, etc.). – jarvisccc Feb 21 '18 at 18:57
  • 1
    Say you're at /search/{query2} and press back... The URL will change to /search/{query1} and because the document context is still the same one as from /search/{query1}, a popstate event fires. In contrast, when you're at /books/harry-potter/ and press back, the URL will change and the entire document will be replaced with /search/{query2} - resulting in no popstate event to fire since it's a different documents context. If a user was on your search page & then went straight to google, then pressed back... that "back" action didn't happen on your webpage, so the popstate won't happen either – Darryl Huffman Feb 21 '18 at 21:19
  • Ah, its all about the document context, thank you! So the History API, as i mentioned in my question, indeed makes only sense for single page applications. I couldn't find any info on the web about that (important) point. Quite the opposite, the german mozilla docs are totally misleading: According to them there IS a popstate event fired when you navigate to a complete other page like google and go back. Even in the original english docs you can find this paragraph, but at least crossed out.... https://developer.mozilla.org/en-US/docs/Web/API/History_API#Example_of_pushState()_method – jarvisccc Feb 22 '18 at 11:36
  • @DarrylHuffman But your workaround (extracting the query from the URL) unfortunately wouldn't help me much, because javascript gets not called reliably across all browsers when navigating back/forth. – jarvisccc Feb 22 '18 at 11:43
  • Have you tried using onunload? https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunload Maybe an answer lies there for you! Another workaround would be to extract the URL parameter server-side and feed it into your page, seemingly that could fix the issue. Hopefully a workaround can be found! – Darryl Huffman Feb 22 '18 at 17:33

2 Answers2

8

For anyone wanting a little more explanation as to why the popstate event only occurs in certain scenarios, this helped me:

On the MDN popstate event page it states:

A popstate event is dispatched to the window each time the active history entry changes between two history entries for the same document.

and

The popstate event is only triggered by performing a browser action, such as clicking on the back button (or calling history.back() in JavaScript), when navigating between two history entries for the same document.

So it looks like popstate is only called when navigating to different urls within the same document. This works great for SPAs but not for anything else.

As an additional note, the html5 spec for session history doesn't seem to have much clarity as to when the popstate event is called:

The popstate event is fired in certain cases when navigating to a session history entry.

Which may indicate that different browsers will treat it differently.

I would love more insight from anyone else

dhouston
  • 173
  • 2
  • 9
1

There's at least one way to make it work, based on this excerpt from Mozilla's Working with the history API:

... When the page reloads, the page will receive an onload event, but no popstate event. However, if you read the history.state property, you'll get back the state object you would have gotten if a popstate had fired.

A minimal working example can be found in the snippet below.

Run the code snippet and follow these steps to see it in action:

  1. click "populate" to add some text to the display, dynamically (basically mock AJAX)
  2. click "away" to navigate to example.org (don't worry, this happens inside the iframe)
  3. click the browser's "back" button: the display is restored to its populated state
  4. click browser "back" again to clear the display (initial empty state)

const populate = document.getElementById('populate');
const display = document.getElementById('display');

// connect the button
populate.addEventListener('click', () => {
  const state = 'hit browser back button to clear';
  updateDisplay(state);
  // push state onto history stack
  history.pushState(state, '', '#populated');
});

// listen for popstate event, fired e.g. after using browser back button
window.addEventListener('popstate', (event) => updateDisplay(event.state));

// listen for load event, in case we navigate away and then go back
window.addEventListener('load', () => updateDisplay(history.state), false);

function updateDisplay(state) {
  if (state) {
    display.textContent = state;
  } else {
    display.textContent = '';
  }
}
/* the styling is only cosmetic */

.button {
  text-decoration: underline;
  cursor: pointer;
  color: blue;
}
<div class="button" id="populate">populate</div>
<a class="button" href="https://example.org">away</a>
<div id="display"></div>

As noted here:

The state object can be anything that can be serialized.

djvg
  • 11,722
  • 5
  • 72
  • 103