
| home | AJAX (8) || C#.NET (7) || Coldfusion Development (16) || DHTML (15) || Flash Development (19) || jQuery (8) || MSSQL (2) || UNIX (10) |
| 6.18.08 | jQuery History Plugin |
jQuery plugin project page: http://plugins.jquery.com/project/jHistory
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.
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.
Here’s a demonstration in an IFRAME:
Here’s the CURRENT jquery.history.js jQuery plugin code:
$.history = function ( store ) {
// (initialize) create the hidden iframe if not on the root window.document.body
if ( $(".__historyFrame").length == 0 ) {
// set the history cursor to (-1) - this will be populated with current unix timestamp or 0 for the first screen
$.history.cursor = $.history.intervalId = 0;
// initialize the stack of history stored entries
$.history.stack = {};
// initialize the stack of loading hold flags
$.history._loading = {};
// initialize the queue for loading history fragments in sequence
$.history._queue = [];
// append to the root window.document.body without the src - uses class for toggleClass debugging - display:none doesn't work
$("body").append('<iframe class="__historyFrame" src="blank.html" style="border:0px; width:0px; height:0px; visibility:hidden;" />');
// set the src (safari doesnt load the src if set in the append above) + set the onLoad event for the iframe
$('.__historyFrame').load(function () {
// parse out the current cursor from the location/URL
var cursor = $(this).contents().attr( $.browser.msie ? 'URL' : 'location' ).toString().split('#')[1];
if ( cursor ) {
// remove the cursor from the load queue
var qPos = $.inArray( cursor, $.history._queue );
if ( qPos > -1 )
$.history._queue.splice( qPos, 1 );
// flag that the iframe is done loading the new fragment id
$.history._loading[ cursor ] = false;
}
// 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 () {
// if any cursors in queue - load first cursor (FIFO)
if ( $.history._queue.length > 0 && !$.history._loading[ $.history._queue[0] ] ) {
// flag this queued cursor as loading so this interval will not load more than once
$.history._loading[ $.history._queue[0] ] = true;
// move the history cursor in the hidden iframe to the newest fragment identifier
$('.__historyFrame').contents()[0].location.href =
$('.__historyFrame').contents().attr( $.browser.msie ? 'URL' : 'location' ).toString().replace(/[\?|#]{1}(.*)$/gi, '') +
'?' + $.history._queue[0] + '#' + $.history._queue[0];
} else if ( $.history._queue.length == 0 ) {
// 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' )
$.history.callback( $.history.stack[ cursor ], cursor );
}
}
}, 150);
});
} else { // handle new history entries apre-initialization
// set the current unix timestamp for our history
$.history.cursor = (new Date()).getTime().toString();
// add this cursor fragment id into the queue to be loaded by the checking function interval
$.history._queue.push( $.history.cursor );
// insert into the stack with current cursor
$.history.stack[ $.history.cursor ] = store;
}
}
// pre-initialize the history functionality - if you include this plugin this will be loaded as a singleton at time of the root window.onLoad
$(document).ready( function () { $.history(); } );
})(jQuery);
Download this code: jquery-history/jquery.history.js
Here’s the above demonstration’s instantiation of the plugin:
// 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>
Download this code: jquery-history/history.html
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:
Download this code: jquery-history/cache.php
Or in C#.NET (ASP) version in the form of cache.aspx:
Download this code: jquery-history/cache.aspx
Or usa an Apache .htaccess file to control Expires with mod_expires:
Download this code: jquery-history/htaccess
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.
TODO
| Ibrahim on 7.17.08 at 9am |
|
Thanks for your plugin. Can you please show how we can save an xml string or an xml Jquery object in history, then call it back when it’s appropriate. My Ajax application has next and previous buttons with xml data fetched from a remote server and displayed each time the next button is pressed. Now, if the user is going back and forth without making new requests (using the next button,) then the data can be saved in history and fetched from history. It seems that your plugin may help but I am not sure how. Thanks |
| Jim Palmer on 7.17.08 at 9am |
|
Ibrahim, Good question - this sounds like a good case for this plugin. I’ve put together a little pseudocode to help show how to use it. It basically comes down to whenever you get new xml data from the remote server for either a next or previous click, you need to store that xml data in a history event. That is simply accomplished with a $.history( xmlData )
This will save the current data in the history queue. Here is the hard part, you have to re-write the callback function for handling when the user clicks either the forward or back button which is outlined in the code as the $.history.callback function. Here’s the pseudocode which might help: /* handle next button with history support */
function next () { // … fetch xml data from server var xmlData = getXmlData( ‘next’ ); // … show user new data $.history( xmlData ); // … fetch xml data from server displayXmlData( xmlData ); } // reinstate will be the xmlData for that history event } |
2 Comments