Software
26th December 2015 | By:

Manipulating the browser history [HTML5]

Since the introduction of HTML5 it’s been possible to manage the browser history using the history and history.pushState API. It allows us to change the URL in the browser without the page refreshing. There are a lot of frameworks that already support the history and pushState API such as Backbone.js, Ember.js and Angular.js.

In this post, we’ll be looking at using the Browser push state without resorting to any framework. To do this we first need to have a way to check if the user has access to the history API. We can do this as follows:

if(window.history && window.history.pushState){
	// history API available
}

We can navigate through the available history (this will refresh the page if the previous/next page hasn’t been saved by pushState or replaceState) and we can check how many entries we have available.

1. Basic history API

window.history.back();    // Move history one entry back
window.history.forward(); // Move history one entry forward
window.history.go(-2);    // Move history two entries back
window.history.go(2);     // Move history two entries forward



window.history.length;    // Check how many entries in the browser history

If you will try to move history beyond the available boundaries the browser will handle this event and no action will be triggered.

2. history.pushState API

This allows us to change the actual page URL and it won’t refresh the page. This method is adding a new entry into the history. You can pass three arguments to the pushState method:

– data [required]: The JavaScript object that’s JSON serialized. It’s bound to the particular history entry. The maximum size of the object depends from the browser (Firefox: 640kB, IE: 1MB, Chrome: ~10MB). Each time the browser detects that the entry was inserted using pushState/replaceState the `popstate` event will be fired and the `data` object will be passed as an argument.
– title [optional]: The title of the entry.
– url [optional]: The new URL address that will be saved as the history entry. It won’t refresh the page though.

var data = {html_content: '<div>Hello world</div>'}
history.pushState(data, "title", "/my-awesome-url.html");

3. history.replaceState API

This method is replacing the actual history entry with the one we are passing. It is taking the same arguments as the `pushState` method and the same event `popstate` is fired when the browser detects that the particular history entry has been modified by this method.

var data = {html_content: '<div>Hello world</div>'}
history.replaceState(data, "title", "/my-awesome-url.html");

4. popstate event

This event fires each time the browser detects that the history entry was modified manually by the history pushState/replaceState API. The event’s `state` property contains a copy of the object passed as the first argument in the pushState/replaceState methods.

window.onpopstate = function(ev){
	console.log(ev);
	// Handle popstate
}

5. Workflow

To integrate the history API with our website we need to prepare not only the Javascript side of our app but also the server side. The example below will show you how to handle the `pushState` in a Django app.

We will need to prepare a basic HTML template with a container, and we will be managing this containers’ content dynamically.

5.1 HTML

5.1.1 Main template (file test.html)



</pre>
<pre>	<!doctype html>
	<html class="no-js" lang="">
		<head>
			<meta charset="utf-8">
			<meta http-equiv="X-UA-Compatible" content="IE=edge">
			<title></title>
		</head>
		<body>
			<div>
				<h1>Our pushState page</h1>
				<div id="dynamic_container">
					<!-- Content loaded here -->
					{{ content|safe }}
				</div>
				<div id="nav">
					<a class="ajax" href="/">Home</a>
					<a class="ajax" href="?page=1">Page 1</a>
					<a class="ajax" href="?page=2">Page 2</a>
				</div>
			</div>
			<script type="text/javascript">
				// JS here
			</script>
		</body>
	</html>
	
5.1.2 Items template (file items.html)
	<!doctype html>
	<div>
		<ul>
			{% for obj in objects %}
				<li>{{ obj }}</li>
			{% endfor %}
		</ul>
	</div>
	

5.2 Javascript(using jQuery)

By doing this, we are replacing the initial state with the container content. Next we need to catch clicks to prevent page refresh, load content dynamically and replace the container content.

	var $container = $('#dynamic_container');
	$(document).ready(function(){
		// Replace initial history state
		history.replaceState({content: $container.html()}, "", window.location.toString());
	});



	// Catch clicks
	$('a.ajax').click(function(click_ev){
		click_ev.preventDefault();
		var a_url = $(this).attr('href');



		// Load content dynamically
		$.ajax({
			url: a_url,
			dataType: 'json',
			success: function(json_data){
				$container.html(json_data.content);
				history.pushState({content: json_data.content}, "", a_url);
			}
		});
	});



	window.onpopstate = function(ev){
		var state_obj = ev.state;
		if(state_obj){
			$container.html(state_obj.content);
		}
	}
	

5.3 Python

Handle ajax request on the server. The example below is a simple Django class based view:

		from django.http import HttpResponse
		from django.shortcuts import render
		from django.template import RequestContext
		from django.template.loader import render_to_string
		from django.views.generic import View
		from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger



		class TestView(View):
			template_name = 'templates/test.html'
			template_items = 'templates/items.html'



			def get(self, request):
				page = request.GET.get('page', None)



				objects = TestModel.objects.all()
				paginator = Paginator(objects, 30)



				try:
					page = paginator.page(q_page)
				except PageNotAnInteger:
				# If page is not an integer, deliver first page.
					page = paginator.page(1)
				except EmptyPage:
					# If page is out of range (e.g. 9999), deliver last page of results.
					page = paginator.page(paginator.num_pages)



				# Render content html separately
				items_html = render_to_string(self.template_items, {objects: page.object_list}, context_instance=RequestContext(request))



				if request.is_ajax():
					# If request is made by ajax we need to return just the container content
					return HttpResponse(json.dumps({'content': items_html}));



				# If it is normal request return response like always
				return render(request, self.template_name, {content: items_html})
	

Hope it helps.

Tags: , , , , , ,