History API - How to reload content on back and forward browser clicks


#1

Hi all,

I have a AJAX set up which loads content, then by using the dropdown changes the content. Works good.

Codepen for quick viewing.

What I’m trying to do, by using the HTML5 History API is reshow the content when I press the browser buttons, back and forward. I have a partial setup which displays in the console.log though just not sure how I get the history content to show.

I’ve searched and tried getting this to work, hence my code so far.

I will also have a number of history.pushStates and other getjson/ajax requests which change the url based on other click events within the page. So I need a — one size fits all setup.

Any help would be great thanks.

The pushstate
history.pushState(category,'',window.location.pathname + '?page=' + category);

The popsate is where I have the problems, I think.

window.onpopstate = function(event) {
  console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

Again, codepen to see this in action.

Update
Here is an example of another site, view source for full javascript. They use another load function to reload the content.

Barry


#2

Hi all

I’ve managed to get a working example of the original issue, thanks to some help over on SO. Here is a working example of the browsers forward and back history state with my current setup - jsfiddle

Everything works good for a single instance of AJAX.
The problem I have now is when I introduce a much bigger JSON Object (not shown in this code).

I receive the error:

DataCloneError: The object could not be cloned.

After much reading, I think this has something to do with how much the browser can allocate in memory when using the History API. A solution I’d like to try is with local or sessionstorage . I read somewhere this is used when dealing with bigger objects.

Wondering if anybody can help and how I would go about using sessionstorage with my current setup/code? I’m hoping this will fix it :thinking:

$(function() {
	"use strict";

		// Get results on first load
		$.ajax({
		    url: 'https://swapi.co/api/people/',
		    dataType: 'json'
		})
		  .then(function(data) {
		      var template = $('#r_tpl').html();
		      var html = Mustache.render(template, data);
		      $('#results').html(html);
		  
		      window.history.replaceState(
		        { category: 'Select Category', data },
		        null,
		        ''
		      );
		});

		// Get results on change to dropdown
		$(document).on('change', '#category', function(e){
		  e.preventDefault();
		  var category = this.value; // get selected value
		  $.ajax({
		    url: 'https://swapi.co/api/' + category + '/',
		    dataType: 'json'
		  })
		    .then(function(data) {
		      var template = $('#r_tpl').html();
		      var html = Mustache.render(template, data);
		      $('#results').html(html);

		      history.pushState(
		        // data
		        // title
		        // url
		        { category, data },
		        category,
		        window.location.pathname + '?category=' + category
		      );
		  });
		});

		window.addEventListener('popstate', function(event) {
		    var template = $('#r_tpl').html();
		    var html = Mustache.render(template, event.state.data);
		    $('#results').html(html);
		    $('#category').val(event.state.category);
		  });

	});

Any help would be great, been heavily involved with this for some time :expressionless:

In short, how do I use sessionstorage with the above?
Or maybe the error is caused by something else?

Thanks,
Barry


#3

If this helps, might explain my thinking a bit more.

The state object can be anything that can be serialized. Because Firefox saves state objects to the user’s disk so they can be restored after the user restarts the browser, we impose a size limit of 640k characters on the serialized representation of a state object. If you pass a state object whose serialized representation is larger than this to pushState() , the method will throw an exception. If you need more space than this, you’re encouraged to use sessionStorage and/or localStorage .

This part mainly:

If you need more space than this, you’re encouraged to use sessionStorage and/or localStorage

Source: https://developer.mozilla.org/en-US/docs/Web/API/History_API

Barry


#4

Using the web storage API is pretty straight forward – the only caveat is that you can only store strings, so you have to parse/stringify the data. For example:

var getData = function (category) {
  var data = window.sessionStorage.getItem(category)
  return data ? JSON.parse(data) : {}
}

var setData = function (category, data) {
  window.sessionStorage.setItem(
    category, 
    JSON.stringify(data)
  )
}

$(document).on('change', '#category', function () {
  var category = this.value 

  $.ajax({
    url: 'https://swapi.co/api/' + category + '/',
    dataType: 'json'
  }).done(function (data) {
    var template = $('#r_tpl').html()
    var html = Mustache.render(template, data)

    $('#results').html(html)
    setData(category, data)

    history.pushState(
      category,
      category,
      window.location.pathname + '?category=' + category
    )
  })
})

window.addEventListener('popstate', function (event) {
  var category = event.state
  var data = getData(category)
  var template = $('#r_tpl').html()
  var html = Mustache.render(template, data)

  $('#results').html(html)
  $('#category').val(category)
})

BTW, you usually don’t want to store error messages… so better use .done() instead of .then() here, and handle errors separately in a .fail() callback.


#6

Hi @m3g4p0p
Been doing some work with this, things are taking shape its fixed the problem - big thanks :sunglasses:

A couple of questions.

  1. Within the .done method, I have extended the data object with funcs which holds some important functions to render the templates data.

Example

.done(function (data) {
    $.extend(data, funcs); // this line
    var template = $('#r_tpl').html()
    var html = Mustache.render(template, data)
...

How do I include funcs in the code so it’s available to the stored content and history?

  1. Previously, when the page first loaded I would show items from a separate ajax request and use replaceState . No results show anymore when I click back to the first page.
window.history.replaceState(
    { category: 'Select Category', data },
    null,
    ''
);

I also get the same error again when navigating back to the first page

DataCloneError: The object could not be cloned

How do I change the above snippet (replaceState) to reflect our latest change?

Also wondering how I can update content outside of the #results container, I have a title and description which doesn’t seem to update like the items do.

Cheers,
Barry


#7

That’s not possible with JSON.stringify() alone… you’d have to serialise them manually with .toString(), assign them to the data object, and later re-construct them as new Function()s… but I’d advise against that. It’s very inefficient and not even necessary – just assign them as usual after you received the data from the storage.

You’d have to store the response from that initial request too then.

My snippet from above sets / expects a string as the state… but true, an object would be the correct type here. Also you might need a guard to check if the pop state actually specifies a category:

$(document).on('change', '#category', function () {
  var category = this.value 
  // ...
  history.pushState(
    { category: category },
    category,
    window.location.pathname + '?category=' + category
  )
})

window.addEventListener('popstate', function (event) {
  var category = event.state.category

  if (!category) return
  // ...
})

Well you can any store any strings you like, not just the stringified data:

window.sessionStorage.setItem('title', 'some specific title')
// And in the popstate handler something like
var title = window.sessionStorage.getItem('title') || 'some default title'

#8

I’m not sure, how would I do that?

Example of funcs which is in the same file.

var funcs = {
    formatDate: function () {
    ...
    }
    formatSearch: function () {
    ...
    }
...
}

Again, not sure how to do this :blush:

This is another question I have, if I understand correctly.

I will have other links like ?id=1234 and was unsure if I could/can use the popstate for different pushstates outside of category. I mean, not all pushstates are created by the #category dropdown.

Are you saying by adding

if (!category) return
  // ...

We can use other links not associated with the category?

Well you can any store any strings you like, not just the stringified data:

Cool, this is what I was thinking :smile:

And sorry for all the questions, trying to understand stuff, been hard to get any answers on this subject.

Barry


#9

Another tricky question, if you get chance would be great :nerd:

As mentioned above, I can have links like:

window.location.pathname + '?category=' + category
//also like
window.location.pathname + '?id=' + id

Meaning I will need to accommodate both possibilities:

window.addEventListener('popstate', function (event) {
  var category = event.state
  var data = getData(category)
  var template = $('#r_tpl').html()
  var html = Mustache.render(template, data)

  $('#results').html(html)
  $('#category').val(category)
})

and

window.addEventListener('popstate', function (event) {
  var id = event.state
  var data = getData(id)
  var template = $('#another_tpl').html()
  var html = Mustache.render(template, data)

  $('#anotherResultsConatiner').html(html)
})

Can I use two separate popstates?
Is there a way to combine this into one, maybe using variables for template and html?
And do the same with the getter and setter.

Barry


#10

Really exactly the same way as if you got the data from the AJAX request (using the same helper function from above):

var data = getData(category)
$.extend(data, funcs)

Although now that the data is getting passed around in the app, it would be better not to $.extend() it directly (thus modifying the original object), but create a new extended one like so:

var extendedData = $.extend({}, data, funcs)
var template = $('#another_tpl').html()
var html = Mustache.render(template, extendedData)

For example, as taken from your fiddle:

$.ajax({
  url: 'https://swapi.co/api/people/',
  dataType: 'json'
}).done(function (data) {
  var template = $('#r_tpl').html()
  var html = Mustache.render(template, data)

  setData('people', data)
  $('#results').html(html)

  window.history.replaceState(
    { category: 'people' },
    null,
    ''
  )
})

In fact, those .done() callbacks are doing pretty much the same anyway, so you might refactor this quite a bit… here’s a more DRY version:

// Functions to extend the data with
var funcs = {
  foo: function () {/* ... */},
  bar: function () {/* ... */}
}

// Get data by category from the session storage
var getData = function (category) {
  var data = window.sessionStorage.getItem(category)
  return data ? JSON.parse(data) : {}
}

// Set data by category from the session storage
var setData = function (category, data) {
  window.sessionStorage.setItem(
    category,
    JSON.stringify(data)
  )
}

// Render the data to the DOM
var renderData = function (data) {
  var extendedData = $.extend({}, data, funcs)
  var template = $('#r_tpl').html()
  var html = Mustache.render(template, extendedData)

  $('#results').html(html)
}

// Fetch data by category and update everything
var updateData = function (category) {
  return $.ajax({
    url: 'https://swapi.co/api/' + category + '/',
    dataType: 'json'
  }).done(function (data) {
    setData(category, data)
    renderData(data)
  })
}

// Update the data with an initial category
updateData('people').then(function () {
  history.replaceState(
    { category: 'people' },
    document.title,
    window.location.href
  )
})

// Update the category on change
$(document).on('change', '#category', function () {
  var category = this.value

  updateData(category).then(function () {
    history.pushState(
      { category: category },
      category,
      window.location.pathname + '?category=' + category
    )
  })
})

// Render the data from the history / session storage
window.addEventListener('popstate', function (event) {
  var category = event.state.category
  var data = getData(category)

  renderData(data)
  $('#category').val(category)
})

That guard just causes the category view not to update if there is no category specified in the pop state. So yes, you might also push a different, completely unrelated state… or always push the complete application state, such as

var state = {
  category: 'people',
  id: 1234
}

// ...
$(document).on('change', '#category', function () {
  var category = this.value

  updateData(category).then(function () {
    state.category = category

    history.pushState(
      state,
      category,
      window.location.pathname + '?category=' + category
    )
  })
})

As far as I can tell you’re never reading the URL parameters anyway…? But yes you can also push the complete application state as shown above.


#12

Hi @m3g4p0p

I’ve been working with the latest solution using the functions, as you say - reduce DRY. This is now working extremely well, really helped thanks :sunglasses:
And thanks for the comments which made things easier.

Main question:

I mentioned some links will use id?1234, id?2345 etc.

For those links I have a seperate ajax request to fetch the data for the individual item:

// Single item
$(document).on('click', '.item', function(e){
  e.preventDefault();
    
    var id  = $('div > a').attr("data-id");
    var url = base + "&id=" + id;
  
  	var singlePage = $.ajax({
      dataType: 'json',
      url: url
    });
  
    singlePage.done(function(data) {
      var extendedData = $.extend({}, data, funcs)
	  var template = $('#another_tpl').html()
	  var html = Mustache.render(template, extendedData)
      
      $("#results").html(html);

    });
  
    history.pushState(
    	'',
    	'',
    	window.location.pathname + '?id=' + id
    );
});

When I try and go back nothing is displayed.
What is the correct way/pushstate to use so I can display the id pages like the category pages?

In theory, I could visit a lot of id pages without even looking at the category section.


And not sure I fully understand the below — if this is the way to go.

var state = {
category: ‘people’,
id: 1234
}

Also was wondering if I need to add anything after the return?
You used the dots //...

if (!category) return
  // ...

Is this correct?

// Render the data from the history / session storage
window.addEventListener('popstate', function (event) {
  var category = event.state.category
  var data = getData(category)

  renderData(data)
  $('#category').val(category)
  if (!category) return
})

Barry