Saving State: What To Do When Users Leave
In this era of rich JavaScript applications, so much
focus is given to the features of the application that one
crucial element is often overlooked: What happens when the
user leaves the page? We take it for granted that pages
will look the same when we leave and return, but a new
question merges for sites using rich JavaScript
interaction: If the user leaves and returns to the page,
will the application state be preserved?
The effects of losing application state can range from
minor annoyances like losing what page you're on, to
all-out frustration after losing a carefully-typed message
because you accidentally triggered the browser's back
button. (It's easier than you think-- hitting backspace
when the document is in focus triggers the back button in
most browsers). Couple this with the fact that some users
may expect pages to save form data, because of their prior
experience to that effect, and it becomes apparent that a
robust strategy for preserving application state must be
devised.
Browsers automatically save data entered into form
fields, but all JavaScript variables are lost when the user
leaves the page. Furthermore, any form fields that were
created by JavaScript will also be lost. So, for all but
the most simple applications, JavaScript must have a
strategy for saving state that deals with these
limitations.
Some sites like thesixtyone.com
reside entirely on a single page and capture users' back
button clicks with named anchors. But, try writing a wall
post on Facebook and you'll find that it does not save the
post if you leave the page. Accidentally pressing backspace
is all too easy in cases like this where typing is
involved, which is why sites like Gmail and Blogger warn
users that they will lose data before leaving the page.
How To Warn Users Before Leaving the Page
One way you can do this is by assigning a confirmation
message to the return value of the
window.onbeforeunload event handler. The user will
be presented with two choices, OK and
Cancel, along with a custom message of your
choosing.
In the following example, we regsiter an anonymous
function as the event handler for
window.onbeforeunload, and add our own custom
message:
Using window.onbeforeunload to confirm if a
user wants to leave the page (example 1)
window.onbeforeunload = function() {
return "You will lose any unsaved information.";
};
The browser displays your custom message, given in the
return statement of the onbeforeunload event
handler, along with the browser default message. In
Firefox, the result is:
Are you sure you want to navigate away from this
page?
You will lose any unsaved information.
Press OK to continue, or Cancel to stay on the current
page.
Retaining Data When Users Do Leave the Page
You may opt to silently save the user's data when they
leave the page. This may give a better user experience
since they are not confronted with a choice, and their data
is saved automatically.
This is one of the times where Ajax comes in handy.
However, there are also other ways to do this without using
Ajax, such as cleverly storing information in named anchors
or hidden form fields. We'll examine each of these
practices in more depth, but suffice it to say that the
hidden form fields approach works better for conventional
websites that are spread across many pages, whereas storing
data in named anchors is better for single-page, pure
JavaScript applications.
It turns out that while you could (and should) save
state to the server using Ajax, for some cases you will
want to avoid Ajax altogether and use a simpler,
clientside-only model.
Using Hidden Form Fields to Save State
As mentioned, all JavaScript objects are lost when the
user leaves or refreshes the page. But, browsers will
retain data in form fields, provided that the form elements
were not generated using JavaScript. Given this limitation,
it is necessary to save JavaScript variables (or the
serialized JSON strings of such objects) to hidden form
fields if they need to be retained.
Here is a basic example showing how variables can be
stored to hidden form fields and restored on page load:
Saving data in hidden form fields (example 2)
// The variable userData is some necessary information we need from the user.
// The first time the user visits the page, they must enter this data manually.
// But, when leaving and returning to the page (or refreshing the page), we'll check
// if they already entered the data, and if so, restore it from a hidden form field.
var userData;
// Register event handlers
window.onload = function() {
restoreState();
if (!userData) {
userData = prompt('Please enter the data to save', 'test');
}
document.write("userData: " + userData);
}
window.onbeforeunload = saveState;
// This function is called onbeforeunload and writes the userData to the hidden form field
function saveState() {
document.getElementById('saved-data').value = userData;
}
// This function is called onload and checks if any data is present in the hidden form field
// If so, it defines userData to be the saved data
function restoreState() {
var savedData = document.getElementById('saved-data').value;
if (savedData != "") {
userData = savedData;
}
}
In the above example, all we're saving is one string
from the user. But what about cases where we need to save
many different values? For instance, what if we're using
object-oriented code and have numerous nested objects
within objects we need to store? At times like this,
serializing objects with JSON is the
easiest way to store the data. Without using JSON, you'd
have to create a hidden form field for each value you want
to save, whereas JSON can create string representations of
complex data structures that you can easily eval
back into JavaScript objects once they're fetched from the
DOM.
So What is JSON, Anyway?
JSON (pronounced "Jason"), short for JavaScript
Object Notation is a lightweight, human- and
machine-readable way to represent the string serializations
of objects. These strings can be evaluated back into
JavaScript objects as needed. For instance, say I create a
JavaScript object to represent a person (in this case,
me):
var person = new Object();
person.name = "Jonah";
person.age = 24;
person.gender = "male";
person.location = "Seattle, WA";
The JSON representation of this object is as
follows:
{
'person': {
'name': 'Jonah',
'age': 24,
'gender': 'male',
'location': 'Seattle, WA'
}
}
Then, if you need to reconstruct the object at a later
point, you can simply eval the JSON string:
var jsonString = "{'person': {'name': 'Jonah', 'age': 24, 'gender': 'male', 'location': 'Seattle, WA'}}";
var person = eval( '(' + jsonString + ')' );
console.assert(person.name == 'Jonah');
console.assert(person.age == 24);
console.assert(person.gender == 'male');
console.assert(person.location == 'Seattle, WA');
If you've written JavaScript using object literal syntax
before, this should be familiar to you. The only minor
difference between JSON and the standard JavaScript object
literal syntax is that JSON requires quotes around key in a
key/value pair. So, name is a valid JavaScript key
but in JSON it would have to be 'name'. (Note: It
doesn't matter if you use single- or double-quotes, as long
as they are matched).
Stringifying Objects in JSON
To use JSON, it's necessary to include a library of JSON
methods. Don't worry, the library is quite small. The
entire thing shouldn't be more than 2k and can be obtained
from json.org.
Eventually, the JSON methods will be included as part of
the core JavaScript language, but for the time being, we're
left to use the methods provided by json.org or those found
in libraries such as MooTools, Prototype and jQuery.
Depending on the library used, the method names for
serializing an object into a JSON string are different.
But, they are all used in rather similar fashion. For now,
we'll assume you're using the library from json.org and use the
method names provided in its API.
Saving complex JavaScript data structures as JSON
strings (example 3)
// The variable userData is some necessary information we need from the user.
// The first time the user visits the page, they must enter this data manually.
// But, when leaving and returning to the page (or refreshing the page), we'll check
// if they already entered the data, and if so, restore it from a hidden form field.
var userData;
// Register event handlers
window.onload = function() {
restoreState();
if (!userData) {
userData = new Object();
userData.name = prompt('Please enter a name', 'Jonah');
userData.age = parseInt(prompt('Please enter an age', '24'));
userData.gender = prompt('Please enter a gender', 'male');
userData.location = prompt('Please enter a location', 'Seattle, WA');
}
displayData(userData);
}
window.onbeforeunload = saveState;
// This function is called onbeforeunload and writes the userData to the hidden form field
function saveState() {
document.getElementById('saved-data').value = JSON.stringify(userData);
}
// This function is called onload and checks if any data is present in the hidden form field
// If so, it defines userData to be the saved data
function restoreState() {
var savedData = document.getElementById('saved-data').value;
if (savedData != "") {
userData = eval( '(' + savedData + ')' );
}
}
// This is a helper function that iterates through each property in an object and renders it in HTML.
function displayData(obj) {
var list = document.createElement('ul');
for (var property in obj) {
var text = document.createTextNode(property + ': ' + obj[property])
var line = document.createElement('li');
line.appendChild(text);
list.appendChild(line);
}
document.getElementsByTagName('body')[0].appendChild(list);
}
This example is pretty similar to the previous one where
we saved a string. The only difference is that in this
case, the string is a representation of a complex
JavaScript object. In fact, you can save the entire state
of your application in one JSON string, as long as the
application state is completely stored as properties of a
single object. There are a few minor gotchas, such as
having to add parentheses around the JSON string when
evaluating it. But, overall this is a clean and
straightforward approach that is very useful when complex
data structures must be retained.
Using Named Anchors to Save State
An alternate option for retaining state is to not
actually let the user leave the page at all. Rather, when
following links on the site, update the named anchor
(everything after the number sign in a URL), instead of
changing the actual document being displayed.
The problem that this is trying to solve is the fact
that Ajax applications will normally break the back button.
A user loads the application on the homepage and clicks to
visit a different page, but since the new page is loaded in
via Ajax, the browser URL doesn't change. Then the user
clicks the back button and leaves the application
altogether-- not the intention of the user, who just wanted
to get back to the homepage.
Storing data in named anchors offers a solution to this
problem. Each time the application state changes,
JavaScript updates the named anchor with a token
representing the application state. When the page is
loaded, data is read from the named anchors and the state
can be restored.
Say you're on the homepage of an ecommerce Ajax
application and click on a product you'd like to view.
Instead of changing URLs to the detail page, the
application loads in new data with Ajax. So, when a user
clicks on the new Brad Mehldau CD for instance, instead of
going to a different URL (yoursite.com/brad-mehldau/) the
document URL remains the same, but JavaScript updates the
named anchor: yoursite.com/#brad-mehldau.
One site which does this unbelievably well is thesixtyone.com
(Thanks, Derek!). The entire site resides in one document,
truly a rich JavaScript application if I've ever seen one.
But, despite the fact that the entire application is
contained in a single document URL, due to clever use of
named anchors, the site has full back button support and
you can even email working links to friends.
Implementing code to save state in named anchors is out
of scope for this article, but you can see how it is
somewhat similar to saving data in hidden form fields. In
this case, there are a few more issues to mitigate and it's
somewhat tricky, but the reward is an Ajax site with fully
functional back button support and the ability to share
links -- worth all the effort, in my book.
So What Use is Ajax, Then?
Since we've made it this far, you might think that there
is no use for Ajax in all this. Actually, Ajax is great for
saving state to the server, especially for saving data
beyond the lifespan of the browser session. Ajax can be
used to save messages periodically (like how Gmail and
Google Docs automatically save on a timer every few
minutes). It can also be used to send data when the user
leaves the page by capturing the onbeforeunload
event, but this is unreliable and I would not depend on
this Ajax request to complete. Instead, try to save the
data before the user attempts to leave the page, by either
firing the Ajax request on a timer or another event on the
page (leaving focus on a form element, for example).
Some frameworks like Prototype have serialize() methods
that return URL query string representations of objects.
This is perfect for saving data through GET requests. Yes,
GET requests have a 2000-character limit and other
limitations, but in most cases this won't be an issue. Even
without helper methods to serialize objects, it's a fairly
simple matter to construct an Ajax request that will save
the necessary data to the server.
Wrapping Up
To re-cap, it is a good practice to check if users are
sure they want to leave a page when they are entering
information, but it's even better to silently save that
information for them. (Arguably you would want to do both,
like how Gmail and Blogger save state and ask
users if they are sure they want to leave the page). There
are many different ways to save state, some purely
client-side and others relying on saving data to the server
with Ajax. The solutions which save data to the server are
suitable for times when the data needs to be saved beyond
the browsing session.
Of the two client-side solutions explored, hidden form
fields and named anchors, the former is more suitable for
conventional websites spanning many pages while the latter
better suits single-page Ajax applications. Using named
anchors also has the added benefit of allowing users to
bookmark and send links to the JavaScript application in
various states, and the state is preserved beyond the
browsing session.
Whatever strategy you follow, your users will thank you
for the time saved and frustration avoided of having to
re-enter lost information.
In this era of rich JavaScript applications, so much focus is given to the features of the application that one crucial element is often overlooked: What happens when the user leaves the page? We take it for granted that pages will look the same when we leave and return, but a new question merges for sites using rich JavaScript interaction: If the user leaves and returns to the page, will the application state be preserved?
The effects of losing application state can range from minor annoyances like losing what page you're on, to all-out frustration after losing a carefully-typed message because you accidentally triggered the browser's back button. (It's easier than you think-- hitting backspace when the document is in focus triggers the back button in most browsers). Couple this with the fact that some users may expect pages to save form data, because of their prior experience to that effect, and it becomes apparent that a robust strategy for preserving application state must be devised.
Browsers automatically save data entered into form fields, but all JavaScript variables are lost when the user leaves the page. Furthermore, any form fields that were created by JavaScript will also be lost. So, for all but the most simple applications, JavaScript must have a strategy for saving state that deals with these limitations.
Some sites like thesixtyone.com reside entirely on a single page and capture users' back button clicks with named anchors. But, try writing a wall post on Facebook and you'll find that it does not save the post if you leave the page. Accidentally pressing backspace is all too easy in cases like this where typing is involved, which is why sites like Gmail and Blogger warn users that they will lose data before leaving the page.
How To Warn Users Before Leaving the Page
One way you can do this is by assigning a confirmation message to the return value of the window.onbeforeunload event handler. The user will be presented with two choices, OK and Cancel, along with a custom message of your choosing.
In the following example, we regsiter an anonymous function as the event handler for window.onbeforeunload, and add our own custom message:
Using window.onbeforeunload to confirm if a user wants to leave the page (example 1)
window.onbeforeunload = function() { return "You will lose any unsaved information."; };
The browser displays your custom message, given in the return statement of the onbeforeunload event handler, along with the browser default message. In Firefox, the result is:
Are you sure you want to navigate away from this page?
You will lose any unsaved information.
Press OK to continue, or Cancel to stay on the current page.
Retaining Data When Users Do Leave the Page
You may opt to silently save the user's data when they leave the page. This may give a better user experience since they are not confronted with a choice, and their data is saved automatically.
This is one of the times where Ajax comes in handy. However, there are also other ways to do this without using Ajax, such as cleverly storing information in named anchors or hidden form fields. We'll examine each of these practices in more depth, but suffice it to say that the hidden form fields approach works better for conventional websites that are spread across many pages, whereas storing data in named anchors is better for single-page, pure JavaScript applications.
It turns out that while you could (and should) save state to the server using Ajax, for some cases you will want to avoid Ajax altogether and use a simpler, clientside-only model.
Using Hidden Form Fields to Save State
As mentioned, all JavaScript objects are lost when the user leaves or refreshes the page. But, browsers will retain data in form fields, provided that the form elements were not generated using JavaScript. Given this limitation, it is necessary to save JavaScript variables (or the serialized JSON strings of such objects) to hidden form fields if they need to be retained.
Here is a basic example showing how variables can be stored to hidden form fields and restored on page load:
Saving data in hidden form fields (example 2)
// The variable userData is some necessary information we need from the user. // The first time the user visits the page, they must enter this data manually. // But, when leaving and returning to the page (or refreshing the page), we'll check // if they already entered the data, and if so, restore it from a hidden form field. var userData; // Register event handlers window.onload = function() { restoreState(); if (!userData) { userData = prompt('Please enter the data to save', 'test'); } document.write("userData: " + userData); } window.onbeforeunload = saveState; // This function is called onbeforeunload and writes the userData to the hidden form field function saveState() { document.getElementById('saved-data').value = userData; } // This function is called onload and checks if any data is present in the hidden form field // If so, it defines userData to be the saved data function restoreState() { var savedData = document.getElementById('saved-data').value; if (savedData != "") { userData = savedData; } }
In the above example, all we're saving is one string from the user. But what about cases where we need to save many different values? For instance, what if we're using object-oriented code and have numerous nested objects within objects we need to store? At times like this, serializing objects with JSON is the easiest way to store the data. Without using JSON, you'd have to create a hidden form field for each value you want to save, whereas JSON can create string representations of complex data structures that you can easily eval back into JavaScript objects once they're fetched from the DOM.
So What is JSON, Anyway?
JSON (pronounced "Jason"), short for JavaScript Object Notation is a lightweight, human- and machine-readable way to represent the string serializations of objects. These strings can be evaluated back into JavaScript objects as needed. For instance, say I create a JavaScript object to represent a person (in this case, me):
var person = new Object(); person.name = "Jonah"; person.age = 24; person.gender = "male"; person.location = "Seattle, WA";
The JSON representation of this object is as follows:
{ 'person': { 'name': 'Jonah', 'age': 24, 'gender': 'male', 'location': 'Seattle, WA' } }
Then, if you need to reconstruct the object at a later point, you can simply eval the JSON string:
var jsonString = "{'person': {'name': 'Jonah', 'age': 24, 'gender': 'male', 'location': 'Seattle, WA'}}"; var person = eval( '(' + jsonString + ')' ); console.assert(person.name == 'Jonah'); console.assert(person.age == 24); console.assert(person.gender == 'male'); console.assert(person.location == 'Seattle, WA');
If you've written JavaScript using object literal syntax before, this should be familiar to you. The only minor difference between JSON and the standard JavaScript object literal syntax is that JSON requires quotes around key in a key/value pair. So, name is a valid JavaScript key but in JSON it would have to be 'name'. (Note: It doesn't matter if you use single- or double-quotes, as long as they are matched).
Stringifying Objects in JSON
To use JSON, it's necessary to include a library of JSON methods. Don't worry, the library is quite small. The entire thing shouldn't be more than 2k and can be obtained from json.org. Eventually, the JSON methods will be included as part of the core JavaScript language, but for the time being, we're left to use the methods provided by json.org or those found in libraries such as MooTools, Prototype and jQuery.
Depending on the library used, the method names for serializing an object into a JSON string are different. But, they are all used in rather similar fashion. For now, we'll assume you're using the library from json.org and use the method names provided in its API.
Saving complex JavaScript data structures as JSON strings (example 3)
// The variable userData is some necessary information we need from the user. // The first time the user visits the page, they must enter this data manually. // But, when leaving and returning to the page (or refreshing the page), we'll check // if they already entered the data, and if so, restore it from a hidden form field. var userData; // Register event handlers window.onload = function() { restoreState(); if (!userData) { userData = new Object(); userData.name = prompt('Please enter a name', 'Jonah'); userData.age = parseInt(prompt('Please enter an age', '24')); userData.gender = prompt('Please enter a gender', 'male'); userData.location = prompt('Please enter a location', 'Seattle, WA'); } displayData(userData); } window.onbeforeunload = saveState; // This function is called onbeforeunload and writes the userData to the hidden form field function saveState() { document.getElementById('saved-data').value = JSON.stringify(userData); } // This function is called onload and checks if any data is present in the hidden form field // If so, it defines userData to be the saved data function restoreState() { var savedData = document.getElementById('saved-data').value; if (savedData != "") { userData = eval( '(' + savedData + ')' ); } } // This is a helper function that iterates through each property in an object and renders it in HTML. function displayData(obj) { var list = document.createElement('ul'); for (var property in obj) { var text = document.createTextNode(property + ': ' + obj[property]) var line = document.createElement('li'); line.appendChild(text); list.appendChild(line); } document.getElementsByTagName('body')[0].appendChild(list); }
This example is pretty similar to the previous one where we saved a string. The only difference is that in this case, the string is a representation of a complex JavaScript object. In fact, you can save the entire state of your application in one JSON string, as long as the application state is completely stored as properties of a single object. There are a few minor gotchas, such as having to add parentheses around the JSON string when evaluating it. But, overall this is a clean and straightforward approach that is very useful when complex data structures must be retained.
Using Named Anchors to Save State
An alternate option for retaining state is to not actually let the user leave the page at all. Rather, when following links on the site, update the named anchor (everything after the number sign in a URL), instead of changing the actual document being displayed.
The problem that this is trying to solve is the fact that Ajax applications will normally break the back button. A user loads the application on the homepage and clicks to visit a different page, but since the new page is loaded in via Ajax, the browser URL doesn't change. Then the user clicks the back button and leaves the application altogether-- not the intention of the user, who just wanted to get back to the homepage.
Storing data in named anchors offers a solution to this problem. Each time the application state changes, JavaScript updates the named anchor with a token representing the application state. When the page is loaded, data is read from the named anchors and the state can be restored.
Say you're on the homepage of an ecommerce Ajax application and click on a product you'd like to view. Instead of changing URLs to the detail page, the application loads in new data with Ajax. So, when a user clicks on the new Brad Mehldau CD for instance, instead of going to a different URL (yoursite.com/brad-mehldau/) the document URL remains the same, but JavaScript updates the named anchor: yoursite.com/#brad-mehldau.
One site which does this unbelievably well is thesixtyone.com (Thanks, Derek!). The entire site resides in one document, truly a rich JavaScript application if I've ever seen one. But, despite the fact that the entire application is contained in a single document URL, due to clever use of named anchors, the site has full back button support and you can even email working links to friends.
Implementing code to save state in named anchors is out of scope for this article, but you can see how it is somewhat similar to saving data in hidden form fields. In this case, there are a few more issues to mitigate and it's somewhat tricky, but the reward is an Ajax site with fully functional back button support and the ability to share links -- worth all the effort, in my book.
So What Use is Ajax, Then?
Since we've made it this far, you might think that there is no use for Ajax in all this. Actually, Ajax is great for saving state to the server, especially for saving data beyond the lifespan of the browser session. Ajax can be used to save messages periodically (like how Gmail and Google Docs automatically save on a timer every few minutes). It can also be used to send data when the user leaves the page by capturing the onbeforeunload event, but this is unreliable and I would not depend on this Ajax request to complete. Instead, try to save the data before the user attempts to leave the page, by either firing the Ajax request on a timer or another event on the page (leaving focus on a form element, for example).
Some frameworks like Prototype have serialize() methods that return URL query string representations of objects. This is perfect for saving data through GET requests. Yes, GET requests have a 2000-character limit and other limitations, but in most cases this won't be an issue. Even without helper methods to serialize objects, it's a fairly simple matter to construct an Ajax request that will save the necessary data to the server.
Wrapping Up
To re-cap, it is a good practice to check if users are sure they want to leave a page when they are entering information, but it's even better to silently save that information for them. (Arguably you would want to do both, like how Gmail and Blogger save state and ask users if they are sure they want to leave the page). There are many different ways to save state, some purely client-side and others relying on saving data to the server with Ajax. The solutions which save data to the server are suitable for times when the data needs to be saved beyond the browsing session.
Of the two client-side solutions explored, hidden form fields and named anchors, the former is more suitable for conventional websites spanning many pages while the latter better suits single-page Ajax applications. Using named anchors also has the added benefit of allowing users to bookmark and send links to the JavaScript application in various states, and the state is preserved beyond the browsing session.
Whatever strategy you follow, your users will thank you for the time saved and frustration avoided of having to re-enter lost information.
1 Comments:
Hi Jonah, i have a problem when i retrieve the control state i check if the document.getElementById("sectionState").value if is not have a value like this
if(document.getElementById("sectionState").value != "")
{
savedData = document.getElementById("sectionState").value;
}
and it always null i save the state i don't understand why it alwayas i use asp.net i fill this controls with a callback and i do a postback and nothing happend its always null the getElementByIdValue
Post a Comment
Subscribe to Post Comments [Atom]
<< Home