Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AJAX content and browser navigation buttons

Hi have the following jQuery event:

listItems.on('click', function(ev) {
  ev.preventDefault();
  reload = true;
  var url = jQuery(this).attr('data-url');
  history.pushState({}, '', url);
  ...
}

Which load dynamic content via AJAX and pushes history state to modify URL in browser from www.domain.com to www.domain.com/page-x. The only reason im loading content via AJAX is that i need to animate page transitions. If you go to www.domain.com/page-x you get normal HTML page.

The problem occurs if after clicking listItem user clicks back button on it's browser. The url changes back from www.domain.com/page-x to www.domain.com, but the page doesn't reload. Thats why i added this event:

window.onpopstate = function() {
  if (reload == true) {
    reload = false;
    location.reload();
  }
}

It reloads page after url has changed, if current page was loaded via AJAX. It's working fine, but now i have other problem:

  1. User in frontpage clicks list item;
  2. Browser URL changes, content is loaded via AJAX and animated;
  3. User clicks BACK browser button;
  4. Browser URL changes to previous and page is reloaded;
  5. User clicks FORWARD browser button, URL changes but nothing happens;

Any ideas/solutions would be highly appreciated.

like image 672
di3sel Avatar asked Feb 27 '14 08:02

di3sel


1 Answers

Rather than using reload=true you should rely on event.state so that when popstateoccurs you get a snapshot of what you recorded for that URL.

Manipulating the browser history (MDN)

For example:

listItems.on('click', function(ev) {
  ev.preventDefault();
  var url = jQuery(this).attr('data-url');
  history.pushState({ action: 'list-item-focused' }, '', url);
  ...
}

And then:

window.onpopstate = function(e) {
  /// this state object will have the action attribute 'list-item-focused'
  /// when the user navigates forward to the list item. Meaning you can
  /// do what you will at this point.
  console.log(e.state.action);
}

You probably should avoid a full page reload when the user hits back, and instead animate your content back that used to be there, that way you aren't trashing the page. You can then tell the difference between the landing page and the list pages by checking for event.state.action and code your responses accordingly.

window.onpopstate = function(e) {
  if ( e.state.action == 'list-item-focused') {
    /// a list item is focused by the history item
  }
  else {
    /// a list item isn't focused, so therefore landing page
    /// obviously this only remains true whilst you don't
    /// make further pushed states. If you do, you will have to
    /// extend your popstate to take those new actions into account.
  }
}

I'm sure you are aware of this, but pushState isn't fully cross-browser, so you should also anticipate what could happen for users if these methods aren't supported.


further enhancements

Also, as you are using jQuery, it makes it quite easy for your to store further useful information in the state object, that may help you enhance your reactions in popstate:

history.pushState({
  action: 'list-item-focused',
  listindex: jQuery(this).index()
});

You have to bear in mind that any data you store is serialised, meaning that it will most likely be converted to some form of non-interactive string or binary data. This means you can't store references to elements or other "live" instances; hence the fact I'm storing the list items index instead.

With the above you now know what action was occurring and on what list item, which you can retrieve at the other end by using:

listItems.eq(event.state.listindex)


what to do onload?

MDN has this to say about what you should do onload.

Reading the current state

When your page loads, it might have a non-null state object. This can happen, for example, if the page sets a state object (using pushState() or replaceState()) and then the user restarts her browser. When your 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.

You can read the state of the current history entry without waiting for a popstate event using the history.state property like this:

Put simply they are just recommending that you should listen out for the current history.state when the page loads, and act accordingly based on what your state.action describes. This way you support both events that are triggered within a page's life-time i.e. popstate and when a user directly jumps to a history state which causes a page load.

So with the above in mind it would probably be best to structure things like so:

window.onpopstate = function(e) {
  reactToState(e.state);
}

window.onload = function(){
  history.state && reactToState(history.state);
}

var reactToState = function(state){
  if ( state.action == 'list-item-focused') {
    /// a list item is focused by the history item
  }
  else {
    /// a list item isn't focused, so therefore landing page
    /// obviously this only remains true whilst you don't
    /// make further pushed states. If you do, you will have to
    /// extend your popstate to take those new actions into account.
  }
}

I've used inline event listeners for simplicity and because your examples do too, however it would be advised to use the addEventListener and attachEvent (IE only) methods instead... or better still, because you are using jQuery.

jQuery(window)
  .on('popstate', function(e) {
    reactToState(e.originalEvent.state);
  }
  .on('load', function(){
    history.state && reactToState(history.state);
  }
;


switching states

Obviously in order to move between two states, you need to know what both those states are; this means having some way to record your current state — so you can compare with the new state and act accordingly. As you only have two states this may not be imperative, but I'd urge you to always be thinking forward about the possibility of having more complexity than you currently have.

I do not know the layout of your code, so it makes it tricky recommending where you should place variables and other such items. However, no matter how you store the information, it doesn't change the fact that it will make your life and code easier if you do:

var reactToState = function(state){
  var currentState = reactToState.currentState || {};
  if ( !currentState.action ) { currentState.action = 'start'; }
  if ( state.action == 'list-item-focused') {
    if ( currentState.action == 'start' ) {
      /// here we know we are shifting from the start page to list-item
    }
  }
  else {
    if ( currentState.action == 'list-item-focused' ) {
      /// here we know we are shifting from the list-item to the start
    }
  }
  /// as the state will be global for your application there is no harm
  /// in storing the current state on this function as a "static" attribute.
  reactToState.currentState = state;
}

Better yet, if you're not averse to switch statements, you can make the above more readable:

var reactToState = function(state){

  /// current state with fallback for initial state
  var currentState = reactToState.currentState || {action: 'start'};

  /// current to new with fallback for empty action i.e. initial state
  var a = (currentState.action||'start');
  var b = (state.action||'start');

  switch ( a + ' >> ' + b ) {
    case 'start >> list-item-focused':
      /// animate and update here
    break;
    case 'list-item-focused >> start':
      /// animate and update here
    break;
  }

  /// remember to store the current state again
  reactToState.currentState = state;
}
like image 194
Pebbl Avatar answered Sep 23 '22 02:09

Pebbl