CURRENT PROJECTS
loading
CATEGORIES AND POSTS
loading
overset
DEVELOPMENT LOG FOR JIM PALMER
Posted 06/18/2008 in jquery


This has been a problem for some time - interactive pages need to somehow harness the forward/back browser button functionality in a seamless way that doesn't alter the user experience. This solution is a simple to use jQuery plugin only 3.2KB in size for the development version.

GOAL

to seamlessly offer forward/back button support for all/any AJAX calls that need to maintain state. That means complete control of when the forward/back button entry is added and that the current "state" of the application is stored associated with that history entry for potential reinstatement.

There are so many versions of what many consider just simple "history" support and I've gone through several revisions of ones I've built for myself. There hasn't been one, beyond from my own work, that has the right support and feature set. All the javascript API plugins and OEM support for this type of "history" utilize the well documented and explored "fragment identifier" methodolgy. That essentially means forcing the user to browse a page via "#" anchor links. This also creates fast ways to bookmark specific functionality at the expense of the user seeing a "#ID" upon every "history" entry in their browser's location bar. As it turns out most of my applications do not require the ability to bookmark specific "fragment identifiers" nor do I want the user to ever see them in the location bar.

The "fragment identifiers" methodology is still what I used because it cannot be beat in terms of speed. A previous version of my history plugin used FORM POSTs via an IFRAME to a remote script to store application data in the session, respond with a POST REDIRECT GET response, then instruct the browser to store the entire action in it's default history. This worked like a charm in all browsers, but the performance penalty was extreme, especially with the PRG doubling the number of requests. The fragment identifier is by far faster because no requests are made. Then came the problem of how do we hide this from the user and then came the every-classy hidden IFRAME.

SOLUTION

To allow for the programmer to make a single function call to have any state data saved through controlling the location of a hidden iframe through a safe queue. This allows forward/back support, javascript state/data support and support for queuing the storage of such history entries via a queue.

CURRENT VERSION
0.6 - 3.2KB development version

DEMO
Click the raiseCounter() button at your leisure and then use your browser's back/forward button
loading demo..


Here's the CURRENT jquery.history.js jQuery plugin code:
/*
**  jhistory 0.6 - jQuery plugin allowing simple non-intrusive browser history
**  author: Jim Palmer; released under MIT license
**    collage of ideas from Taku Sano, Mikage Sawatari, david bloom and Klaus Hartl
**  CONFIG -- place in your document.ready function two possible config settings:
**    $.history._cache = 'cache.html'; // REQUIRED - location to your cache response handler (static flat files prefered)
**    $.history.stack = {<old object>}; // OPTIONAL - prefill this with previously saved history stack (i.e. saved with session)
*/
(function($) {
	// initialize jhistory - the iframe controller and setinterval'd listener (pseudo observer)
	$(function () {
		// create the hidden iframe if not on the root window.document.body on-demand
		$("body").append('<iframe class="__historyFrame" src="' + $.history._cache +
			'" style="border:0px; width:0px; height:0px; visibility:hidden;" />');
		// setup interval function to check for changes in "history" via iframe hash and call appropriate callback function to handle it
		$.history.intervalId = $.history.intervalId || window.setInterval(function () {
				// fetch current cursor from the iframe document.URL or document.location depending on browser support
				var cursor = $(".__historyFrame").contents().attr( $.browser.msie ? 'URL' : 'location' ).toString().split('#')[1];
				// display debugging information if block id exists
				$('#__historyDebug').html('"' + $.history.cursor + '" vs "' + cursor + '" - ' + (new Date()).toString());
				// if cursors are different (forw/back hit) then reinstate data only when iframe is done loading
				if ( parseFloat($.history.cursor) >= 0 && parseFloat($.history.cursor) != ( parseFloat(cursor) || 0 ) ) {
					// set the history cursor to the current cursor
					$.history.cursor = parseFloat(cursor) || 0;
					// reinstate the current cursor data through the callback
					if ( typeof($.history.callback) == 'function' ) {
						// prevent the callback from re-inserting same history element
						$.history._locked = true;
						$.history.callback( $.history.stack[ cursor ], cursor );
						$.history._locked = false;
					}
				}
			}, 150);
	});
	// core history plugin functionality - handles singleton instantiation and history observer interval
	$.history = function ( store ) {
		// init the stack if not supplied
		if (!$.history.stack) $.history.stack = {};
		// avoid new history entries when in the middle of a callback handler
		if ($.history._locked) return false;
		// set the current unix timestamp for our history
		$.history.cursor = (new Date()).getTime().toString();
		// insert copy into the stack with current cursor
		$.history.stack[ $.history.cursor ] = $.extend( true, {}, store );
		// force the new hash we're about to write into the IE6/7 history stack
		if ( $.browser.msie )
			$('.__historyFrame')[0].contentWindow.document.open().close();
		// write the fragment id to the hash history - webkit required full href reset - ie/ff work with simple hash manipulation
		if ( $.browser.safari )
			$('.__historyFrame').contents()[0].location.href = $('.__historyFrame').contents()[0].location.href.split('?')[0] +
				'?' + $.history.cursor + '#' + $.history.cursor;
		else
			$('.__historyFrame').contents()[0].location.hash = '#' + $.history.cursor;
	}
})(jQuery);

Here's the above demonstration's instantiation of the plugin:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
	<script language="JavaScript" type="text/javascript" src="jquery.js"></script>
	<script language="JavaScript" type="text/javascript" src="jquery.history.js"></script>
	<script>
		// set our global counter
		var counter = 0;

		// function to raise the counter and then store the change in the history
		function raiseCounter() {
			counter++;
			// store the counter inside an object such as {counter:0} along with extra to test speed
			$.history( {'counter':counter, 'counter1':counter, 'counter2':counter, 'counter3':counter, 'counter4':counter} );
			$('#counter').html('{\'counter\':' + counter.toString() + '}');
		}

		// function to handle the data coming back from the history upon forw/back hit
		$.history.callback = function ( reinstate, cursor ) {
				// check to see if were back to the beginning without any stored data
				if (typeof(reinstate) == 'undefined')
					counter = 0;
				else
					counter = parseInt(reinstate.counter) || 0;
				$('#counter').html('{\'counter\':' + counter.toString() + '}');
			};

		// initialize the display of the counter value on window.onLoad
		$('document').ready(function () {
				$('#counter').html('{\'counter\':' + counter.toString() + '}');
			});
	</script>
</head>
<body>
	<table width="100%">
		<tr>

			<tr>
				<td align=left valign=middle style="padding:10px; background:#EEE;">
					<input type=button value="raiseCounter()" onclick="raiseCounter()">
				</td>
				<td align=left id="counter" valign=middle style="font-family:tahoma; font-size:14pt; padding:10px; background:#EEE;"></td>
				<td align=left id="__historyDebug" valign=middle width="100%" style="font-family:tahoma; font-size:9pt; padding:10px; background:#EEE;"></td>
			<tr>
		</tr>
	</table>
</body>
</html>


Basic Usage

Step 1 Include the plugin after jquery
<script language="JavaScript" type="text/javascript" src="jquery.js"></script>
<script language="JavaScript" type="text/javascript" src="jquery.history.js"></script>
Step 2 Setup a callback function to handle when the user clicks the forward/back buttons and determine what to do with the old data, aka "reinstate"
$.history.callback = function ( reinstate, cursor ) {
	// reinstate is the actual object to reinstate
	// cursor is the actual object (new Date()).getTime() timestamp of the history event
}
Step 3 Store items in the history which is controlled by you
$.history(myUpdatedObject);

The hidden iframe content must be driven by some type of content. This can be as simple as static blank.html file served from the same location. This could be a simple script that replies with headers that ensure that each history entry will be cached by the browser after each load for the time of the user's session expiration. This could be even a more complex script that re-instantiates the latest stored session from a DB upload initial browser reload as well as handle the individual history entries. I lean more towards using a static flat file that resides on a quality CDN and rely on the browser's default caching mechanism.

For our purposes here I'll include several examples of how to create a blank page to be loaded with headers instructing the browser to store the page for the duration of a 20min session timeout.

Or in PHP version in the form of cache.php:
<?php
header("HTTP/1.1 304 Not Modified");
//header("Status: 304 Not Modified");
//  header("Expires: " . gmdate("D, d M Y H:i:s", time() + 1200) . " GMT");
?>

Or in C#.NET (ASP) version in the form of cache.aspx:
<%@ Page Language="C#" AutoEventWireup="true" ValidateRequest="false" %>
<script runat="server">
	protected void Page_Load(object sender, System.EventArgs e) {
		Response.Expires = 1200;
	}
</script>
<html />

Or us a an Apache .htaccess file to control Expires with mod_expires:
<FilesMatch "^blank\.html$">
	ForceType text/html
	ExpiresActive On
	ExpiresByType text/html "access plus 20 minutes"	
</FilesMatch>

Or in COLDFUSION (CFMX8) version in the form of cache.cfm:
<cfheader name="Expires" value="#GetHttpTimeString( DateAdd( "n", 45, Now() ) )#">

To use the plugin, simply include it the same as you would jQuery itself or any other jQuery plugin and call the $.history( ) for every time you want to add an entry to the forward/back history as well as store the associated along with it.

jQuery plugin project page: http://plugins.jquery.com/project/jHistory

TODO
  • build in autosave feature for ajax state management on remote server with session-based handler
  • build cache control scripts in python, perl, ruby, etc
comments
loading
new comment
NAME
EMAIL ME ON UPDATES
EMAIL (hidden)
URL
MESSAGE TAGS ALLOWED: <code> <a> <pre class="code [tab4|tabX|inline|bash]"> <br>
PREVIEW COMMENT
TURING TEST
gravatar