Dirty Forms and The UserData Persistence Behavior with XMLDocument Property

By Peter A. Bromberg, Ph.D.

Peter Bromberg

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