This is my personal blog. The views expressed on these pages are mine alone and not those of my employer.

Thursday, August 25, 2005

AJAX Tutorial: Saving Session Across Page Loads Without Cookies, On The Client Side

This is a mini-tutorial on saving state across page loads on the client side, without using cookies so as to save large amounts of data beyond cookies size limits.

In AJAX, most of the action occurs inside a single web page. When a page is loaded, a new instance of the JavaScript interpreter is started. When you leave a page, jumping to Google for example, all of your JavaScript objects are completely cleared away; you lose all of your state. If you then hit the back button to go back from Google to your original AJAX application, you will find that the page actually completely reloads, calling your onload listener, and that any JavaScript objects you stored anywhere are gone.

This can be a pain. First, it's something that not all programmers are aware of, which can lead to errors, so it's important to know about. Second, user's see their state completely wiped out; when they go back to their AJAX application with the back button they see the original state of their program, not the last place they left it. Third, this can impact performance, since the AJAX application has to re-retrieve everything from the server rather than use its local state.

Finally, there are some libraries that try to solve the back/forward button issues that store local JavaScript state in order to do so; this means they are clobbered if you leave the page, losing all history state. There are some back/forward button libraries that work without internal state, loading everything from the anchor hash on a URL on each change, but these libraries are restricted in the amount of state information you can represent in a URL. To find a truly robust solution to the back/forward button issue, one that allows for large, sophisticated blocks of state to be passed to programmers on each history change, we must find a way to solve the state problem.

Are there ways to save state on the client between page loads, akin to a user-level session? This is something I have been exploring recently. Note that session level state is different than the holy grail of saving offline information; they are similar, but session level state only lasts for a single instance of the browser. When it is closed state is lost, while permanent offline storage of state is saved forever. I have some ideas on how to solve the offline storage issue that I will explore in future blog posts.

I have been surprised to find two different ways to achieve session level state, each with their own stregnths and weaknesses.

Method number one is to use an iframe in some special ways. First, this iframe must exist on page load (See "AJAX Tutorial: A Tale of Two IFrames (or, How To Control Your Browsers History" for details on the different kinds of iframes):
<html>
<body>
<iframe id="sessionFrame"></iframe>
</body>
</html>
Then, whenever you wish to record state that you want to save, you must write that state into the iframe in the following way, programatically:
var sessionFrame = document.getElementById("sessionFrame");
var doc = sessionFrame.contentDocument;
if (doc == undefined) { // Internet Explorer
doc = sessionFrame.contentWindow.document;
}

doc.open();
doc.write(someNewState);
doc.close();
What this does is to actually write your state into the browser's history queue. The doc.write() business creates an entirely fresh new document each time, which the browser dutifully saves into its history list. As you move around with the back and forward buttons, you will see the iframe update with each state change you have saved into the iframe. Every state change must be done as above, with an open(), write(), and close() sequence to create a fresh document.

The full code is a bit more complicated:
<html>
<head>
<script language="JavaScript">
function initialize() {
if (sessionExists() == false) {
saveState("Hello World 1");
// some browsers need a bit of a timeout
window.setTimeout("saveState('Hello World 2')", 300);
window.setTimeout("saveState('Hello World 3')", 600);
}
}

function getIFrameDocument() {
var historyFrame =
document.getElementById("historyFrame");
var doc = historyFrame.contentDocument;
if (doc == undefined) // Internet Explorer
doc = historyFrame.contentWindow.document;

return doc;
}

function sessionExists() {
var doc = getIFrameDocument();
try {
if (doc.body.innerHTML == "")
return false;
else
return true;
}
catch (exp) {
// sometimes an exception is thrown if a
// value is already in the iframe
return true;
}
}

function saveState(message) {
// get our template that we will
// write into the history iframe
var templateDiv =
document.getElementById("iframeTemplate");
var currentElement = templateDiv.firstChild;

// loop until we get a COMMENT_NODE
while (currentElement &&
currentElement.nodeType != 8) {
currentElement = currentElement.nextSibling;
}
var template = currentElement.nodeValue;

// replace the templated value in the
// constants with our new value
var newContent =
template.replace(/\%message\%/, message);

// now write out the new contents
var doc = getIFrameDocument();
doc.open();
doc.write(newContent);
doc.close();
}
</script>
</head>

<body onload="initialize()">
<div id="iframeTemplate">
<!--
<html>
<body>
<p>%message%</p>
</body>
</html>
-->
</div>

<iframe id="historyFrame"></iframe>
</body>

<html>
In the code above, we use a template to store what we will stick into the iframe (see "DHTML Templates Tutorial" for details on using templates). We also have a new method, sessionExists, that checks to see if we have ever written anything into this iframe before. If a user goes to this page, goes to Google, and then comes back, our page's onload listener will fire. We don't want to rewrite our values into the session, so we must have a way to know if we have ever written anything before. If we have not, then we write our test values in.

In production code you would hide the iframe.

Give the code a try, and feel free to use it (it's under a BSD license). The code writes three state changes into the iframe. Go to another website, such as Google, and then hit the back button to return to the page; you will see that all of the changes we wrote into the iframe persist. If you close the browser you will see they disappear, or if you type the website's address seperate from the history.

A major problem (or strength) with the iframe approach is that it affects the history, which can be a problem since we don't always want to tie session storage to the history.

The other session approach I've found is a crazy hack that I saw used for something else, in Danny Goodman's book JavaScript and DHTML Cookbook. Danny was looking for a way to pass data between frames, and he found that you could persist data by saving them into hidden text fields.

This is a beautiful (and scary) hack that is a solution to our session state. All the browsers I have tried (IE, Firefox, and Safari) persist any text you put into a form field, even if they are hidden. To save state, then, we can simply write it into hidden form fields!

I have done tests with the Lorem Ipsum generator to see how much data I can store in a textarea in Firefox and IE, and I couldn't find the upper bound. I had the generator give me about 1 meg of data, placed it into a textarea, and then moved away from the page and back and the data was still there! The data disappears when you close the browser, but this is perfect for saving our session state.

There is a small footnote, though. Basicly, the form and the textarea field that stores state must exist on page load; they can not be dynamically created through JavaScript, which makes using them a bit more difficult.

Here is some simple sample code that shows how you can persist state into a hidden form:


<html>
<head>
<script language="JavaScript">
function initialize() {
var sessionField =
document.getElementById("sessionField");
var sessionValue = sessionField.value;
if (sessionValue == "empty") {
alert("Storing new session value");
sessionField.value = "Hello World";
}
else {
alert("Old session value: " + sessionValue);
}
}
</script>
</head>

<body onload="initialize()">
<form style="display: none;" id="sessionForm">
<textarea id="sessionField">empty</textarea>
</form>
</body>
</html>


Give the demo a whirl as well. The first time you load the page a popup will appear that we are storing state for the first time. Next, if you reload the page or go to another website and then come back, the popup will reappear but will say that we retrieved the old session value, which is "Hello World".

This code is also free to use, under a BSD license.

Comments:
I could be hugely off base from not knowing enough but is the aspect you called scary that any programmer who knows the right field names can retrieve them, assuming the same person visits both target site and evil programmer site?
 
I'd argue that the scary parts are
what guarantees browsers make about this data, w.r.t lifetime, size, and consistency?

good post, good things to think about
 
Thanks for the pointers.

I was stuck with this for a while until I realised that input elements are only repopulated if they are inside a form.
 
Have you tried this with asp or jsp? I don't think it works with these pages.
 
For some reason, this example doesn't work if I try to recreate it in Ruby on Rails using IE. If I take the small example and save it into a regular .html file served through Apache or lighttpd, it works perfectly. But if I take the same file and copy it into a ruby on rails .rhtml file then it stops working. This only affects IE. The browser claims that the source of both files is the same.

It evens happens when I serve the file through a fresh rails install over webrick. There's something about Rails (rewriting? routes?) that is preventing IE from repopulating the form fields with Rails. Will test further.
 
I had the same issue as Tyler and found the following:
Persistance of form fields when the refresh button is clicked appears to be dependant on the presence of an 'ETag' HTTP Header. By default IIS does not add an Etag (or 'Expires' or 'Cache Control' header) for mapped extensions as they are assumed dynamic. (I'd also mapped the .html ext to the asp.net isapi). When I added the etag header (using Response.Cache.SetETag in asp.net) form values were persisted and the above examples worked correctly.
 
Visit AJAX projects for more tutorials, projects, news and sample codes...
www.ajaxprojects.com
 
Ajax Projects will help you to find all the resource to learn and know allabout Ajax technology, by providing you with the latest Ajax projects,latest Ajax tutorial, Ajax articles, Ajax Forum and Ajax news.

www.ajaxprojects.com
 
Great tutorial, hey man and other blog followers, I have a site that is giving forum based classes on PHP, MySQL, Linux, Apache, JavaScript and most importantly AJAX.

I would love to have you guys come check it out learn, and actually give some of your lessons and tutorials on the forum for others to learn and read about it, feel free to use your blog address as a sig when you post new lessons or questions and comments.

I just put up my first lesson, in creating a Shout Box in AJAX so come check it out.

Grim1208
LAMP Geekz
 
This comment has been removed by a blog administrator.
 
I'm not able to make this work for dynamic pages in IE 6 -- upon returning to the page the forms are all reset! Even when my JSP pages are actually called with a '.htm' URL, I get this problem (using Tomcat only, and Apache + tomcat).


It looks like there are caching header issues outlined above. Any summary updates or fixes?

I can't pull it together, so I'm using cookies for now... but this method would be much better if I could get it to work in I.E.
 
In Firefox 1.5.04 it doesnt work..

A bug??
 
I don't understand this. So I have this iframe but then i also have a text box. The iframe contents remembers the change history but the textbox doesn't. The textbox text just stays the same. Is the idea to put the text inside the iframe and then get it out when the user clicks back? Can someone please help me?
 
Nice post man! Will post some comments later, let me experiment a bit.
 
I would never pass my session values in javascript....
Mostly you need Sessions to keep some IDs…
May be I missed something, but it sounds like sending sensitive information in a query string.
Again, sorry, if I missed the point.
 
No more hacks. Simply use CSS:
================

<html>
<head>
<script language="JavaScript">
function initialize() {
var sessionField = document.getElementById("sessionField");
var sessionValue = sessionField.value;
if (sessionValue == "empty") {
alert("Storing new session value");
sessionField.value = "Hello World";
}
else {
alert("Old session value: " + sessionValue);
}
}
</script>
</head>
<body onload="initialize()">
<!--form style="display: none;" id="sessionForm"-->
<form id="sessionForm">
<!--textarea id="sessionField">empty</textarea-->
<div style="position:absolute; z-index:-10; overflow:hidden; width:0; height:0;">
<textarea id="sessionField">empty</textarea>
</div>
</form>
</body>
</html>
 
My previous post "No more hacks. Simply use CSS" especialy for ASP.NET guys and other frameworks.
But don't forget turn on caching in brouser by HTTP headers (in PHP also).
But for me more usefull method with Url anchor serialization, like
http://someurl.dom/page.jsp#someData_adfhaafhgds7h6hh5sd5h87hdh69dsh
In ASP.NET exist AJAX History control for it
 
this works fine in firefox 3.0.1, but when firebug is enabled - no,no : new session is stored at each page load.
 
In the latest webkit/safari 4 the form technique doesn't work because form fields aren't repopulated. The iframe technique as you wrote it also doesn't work, but it's VERY easy to fix.

For the iframe with id "historyFrame" just add an attribute for src="blahblahdasd.html" this forces the iframe to be added to the history. If you leave it blank the states will not be saved to history and WILL NOT persist. You don't even need a real html file (hence the blahblahblahasd.html filename). It will add an extra request hit, so maybe point it to a local non-existant file, I don't know. At most it will add just on more HTTP request and there won't be anything to download so it's not too bad. Anyway that's how I got it to work in Webkit/Safari 4. I didn't try it in Google Chrome, but it's based on webkit so it may fix problems there too.
 
That simply brilliant.
But I have one problem with IE where ajax response are not updated may be it is showing from cache, in mozilla it is ok.

Can some body give me some idea?

Thank you.
Scholarships solutions
 
@Paul i'm using the iframe technique and I have the "src" property of the iframe set but I still can't make it add a history entry to chrome or safari 4
 
@Paul, I've tried setting the iframe src. I've tried a file that exists and one that doesn't exist. I can't get safari 3, 4 and chrome 3 to register the history
 
Post a Comment

Subscribe to Post Comments [Atom]



Links to this post:

Create a Link



<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]