Often you have a complex form on a page
and you need a way to warn the user not to navigate away from the page
before submitting the form, but only if the form has been partially or
even completely filled in . You may also want to have a facility to persist
larger amounts of form data on the client side, and be able to retrieve
this data to repopulate the form automatically when your visitor navigates
back to or reloads the form page. While you're at it, it would be nice
(usually for security reasons) to be able to "Expire" this persisited
form data just as you would a cookie.
There are a set of form events and properties
that allow us to easily write an "isDirty" form function and
use the onBeforeUnload default behavior to warn the errant user who tries
to navigate away from the form page. We can also create a set of generic
"getUserData" and "putUserData" functions that can
be used with any form, to enable us to serialize up to 64K of form (or
other) page data and save it to the client using the IE5 and higher "UserData"
persistence behavior. We can even set an expiration for the UserData object
just as we would for a cookie. But this "cookie on steroids"
has another feature that provides a whole new perspective on client side
data-cacheing -- it automatically exposes an XMLDocument property! This
means we can sort, query or search with XPath, and even apply XSL Tranformations
to the data that we have stored in this object.
I'm going to limit the scope of this article
to writing the "isDirty", the "getUserData" and the
"putUserData" functions, and wiring them up so they can easily
be used on any page. There's plenty of existing documentation on these
issues, both from Microsoft and from outside authors. My objective here
is to try to narrow the scope to a single, more productive example and
to "Genericize" my functions so that they can be easily used
on almost any page.
The key to using the onBeforeUnload event
is to put it in as an event handler call in a tag (usually the Body tag),
like this:
<BODY
onbeforeunload="checkFormStatus(document.forms[0])" >
What
we're telling the browser here is, "before you leave this page (for
any reason) , check to see if document.forms[0] is dirty using the checkFormStatus
function. Our "checkFormStatus" is pretty simple:
function
checkFormStatus(oForm){
if(isDirty(oForm))
event.returnValue = "You have entered form Data without submitting
this form.";
}
The checkFormStatus calls the isDirty function,
passing in a valid form object, and receives a return value of either
true or false. If oForm is "dirty" ( i.e., "true"
-- meaning any of its elements contain new data or have been changed from
their default values), we then set the onBeforeUnload event returnValue
to a string of our choosing. When you set the event.returnValue property
of onBeforeUnload to a string, and only when you do so, will this trigger
the familiar "Are you sure you want to navigate away from this page?"
dialog which also includes your custom string message. This
is why we have this after an "if" test.
Now let's look at the meat of the chili concoction,
the "isDirty" function:
var
bSubmitted=false;
function isDirty(oForm)
{
if(bSubmitted) return false;
var iNumElems = oForm.elements.length;
for (var i=0;i<iNumElems;i++)
{
var oElem = oForm.elements[i];
if ("text" == oElem.type || "TEXTAREA" == oElem.tagName)
{
if (oElem.value != oElem.defaultValue) return true;
}
else if ("checkbox" == oElem.type || "radio" == oElem.type)
{
if (oElem.checked != oElem.defaultChecked) return true;
}
else if ("SELECT" == oElem.tagName)
{
var oOptions = oElem.options;
var iNumOpts = oOptions.length;
for (var j=0;j<iNumOpts;j++)
{
var oOpt = oOptions[j];
if (oOpt.selected != oOpt.defaultSelected) return true;
}
}
}
return false;
}
Notice we've set a global variable "bSubmitted" outside the
function, and set it to false. The reason we want to do this is that onBeforeUnload
gets fired by ANY event that navigates away from the page - including
submitting the form! But we want the form to be able to be submitted in
this case (obviously after any client-side validation processing). So,
by simply inserting an onClick handler in the SUBMIT button --
<input
type=submit id=btnSubmit name=btnSubmit value=SEND onClick="Javascript:bSubmitted=true;">
-- we can have our bSubmitted variable set to true, test
for it at the very beginning of the isDirty function, and exit gracefully.
This is a simplistic solution, I'm sure you can think of a number of other
ways to handle this.
What we do next is pretty basic. We get the number of
form elements, and in a "for" loop, we iterate through them
all, testing to see what type of element they are so we'll know what to
do. If it's type "text", or has a "TEXTAREA" tagName,
we can check to see if it's current value is different from it's defaultValue
property, and return true because the form is "dirty". If it's
type is "checkbox" or "radio", we test it's checked
value against it's defaultChecked property. And if it's tagName is "SELECT",
we know its a ListBox so we iterate through its options collection checking
to see if the selected element equals the defaultSelected.
I think I've covered all the possibilities for form elements
here, but if there are any I've missed, I'm sure you can figure out a
way to test them.
And that pretty much covers how to tell if a form is
dirty and alert the user when they attempt to go bye-bye after doing stuff
with it, but not having submitted it.
Now comes the really cool stuff. We get to re-use some
of the techiques above to store and retrieve our form data with the UserData
behavior.
UserData and all of its built-in behavior brethren, works
like this: We put in a style class declaration in our <HEAD> section:
<STYLE
TYPE="text/css"> .userData {behavior:url(#default#userdata);}</STYLE>
Then
we ATTACH this behavior through a class attribute to an element on the
page. It could be a <DIV>, a <SPAN>, whatever. Here's mine:
<SPAN
CLASS="userData" ID="spnUserData"></SPAN>
Now
here's the "putUserData" function:
function
putUserData(iExpiryMin)
{
var oTimeNow = new Date(); // Start Time
oTimeNow.setMinutes(oTimeNow.getMinutes() + iExpiryMin);
var sExpirationDate = oTimeNow.toUTCString();
spnUserData.expires = sExpirationDate;
var eForm = document.forms[0];
var iNumElems = eForm.elements.length;
for (var i=0;i<iNumElems;i++)
{
oElem=eForm.elements[i];
if ("text" == oElem.type || "TEXTAREA" == oElem.tagName)
spnUserData.setAttribute("el" + i,eForm.elements[i].value);
if ("checkbox" == oElem.type || "radio" == oElem.type)
spnUserData.setAttribute("el" + i,(eForm.elements[i].checked
? 1 : 0));
if ("SELECT" == oElem.tagName)
spnUserData.setAttribute("el" + i,eForm.elements[i].selectedIndex);
}
// alert(spnUserData.xmlDocument.xml);
spnUserData.save("FormData");
}
What we do here is pass in a number of minutes after
the form is submitted to expire the UserData object, and add it to the
Date variable we've assigned to oTimeNow. We then set the "expires"
property of our tag to this value, just as you would with a cookie. Next
we'll iterate through our form collection, doing the same thing we did
with the isDirty function so we can properly get the value of each form
element. As I mentioned, the tag has an xmlDocument property. I've put
in a commented - out alert here so you can actually look at the xml document
if you want. Finally, we call the "save" method on the spnUserData
tag, passing in a name.
The "getUserData" function, which is pretty
much the same thing in reverse, doesn't need much explanation if you're
stil with me at this point:
function
getUserData()
{
spnUserData.load("FormData");
//alert(spnUserData.xmlDocument.xml);
var eForm = document.forms[0];
var iNumOpts = eForm.elements.length;
for (var i=0;i<iNumOpts;i++)
{
oElem =eForm.elements[i];
var sVal = getAtt(i);
if (("text" == oElem.type || "TEXTAREA" == oElem.tagName)
&& sVal !='null')
eForm.elements[i].value = sVal;
if ("checkbox" == oElem.type || "radio" == oElem.type
&& sVal !='null') eForm.elements[i].checked = ("0" ==
sVal ? false : true);
if ("SELECT" == oElem.tagName && sVal !='null') eForm.elements[i].selectedIndex
= sVal;
}
}
The
"getAtt" function in the above simply retrieves the value of
an attribute:
function
getAtt(iNum)
{
var sVal = spnUserData.getAttribute("el" + iNum);
return sVal;
}
We
still need two more "helper" functions, "handleLoad"
and "handleUnload" that tell the page when to call our two UserData
functions:
function
handleLoad()
{
var eForm = document.forms[0];
// clear out any nulls
document.forms[0].reset();
getUserData();
}
function handleUnload()
{
putUserData(1);
}
Then
finally we attach the handleLoad and handleUnload helper functions to
the window.onload and window.onunload events respectively:
window.onload = handleLoad;
window.onunload = handleUnload;
And that completes the whole package.
You can test this out live by trying the full page HERE
To get the code, just save the page - its all there.
In summary, using the form-checking isDirty
routine combined with the onBeforeUnload event can help us prevent users
from losing form data by accidentally navigating away from our page. And
we can substantially enhance our ability to do form data cacheing by wiring
up getUserData and putUserData functions to the UserData default behavior.
Since the behavior exposes an XMLDocument property on the tag we've attached
it to, we now have the ability to sort, search, transform, add to and
subtract from and otherwise manipulate this data, all on the client side.
Shopping carts, recordsets, and other complex objects can be persisted
on the client, saving server roundtrips, and the data cache can be expired
so that it disappears after a certain event or time period has passed.
Peter Bromberg is an independent consultant specializing in distributed .NET solutionsa Senior Programmer
/ Analyst at in Orlando and a co-developer of the NullSkull.com
developer website. He can be reached at info@eggheadcafe.com
|