Using a server, or session storage, to persist form values

This post is more than 2 years old.

Yesterday on Twitter docwisdom asked me about using AJAX to persist form values while you edited data. This is something I've talked about before. I thought though it would be a great example to a) blog it again (I'm a believer in multiple examples, and worse case, the more I work on the client side the more comfortable I get) and b) a great time to compare a server based example versus a completely client side version using HTML5 technology.

Before we begin, it makes sense to talk about why you would want to persist a form before submission anyway. While it (luckily) doesn't happen too often, crashes do occur. It's also possible for a user to simply close a tab by accident. (Not that - um - I've ever done this.)

Let's begin with the first example. In this one, we're going to make use of ColdFusion, HTML, and jQuery. jQuery will be used to notice form changes and will pass it off to the server. The server will simply copy this data to the session scope. The form will be made so that it notices this session information and will make use of it. With me so far? While this is ColdFusion specific, it could be done in other languages using the same techniques I'll employ here. First, the form. It's a bit big, so after the code I'll explain whats going on.


<!--- Default if we have our session vars --->
<cfif not structKeyExists(form, "save") and structKeyExists(session, "formdata")>
	<cfif structKeyExists(session.formdata, "name")>
		<cfset form.name = session.formdata.name>
	<cfelse>
		<cfset form.name = "">
	</cfif>
	<cfif structKeyExists(session.formdata, "email")>
		<cfset form.email = session.formdata.email>
	<cfelse>
		<cfset form.email = "">
	</cfif>
	<cfif structKeyExists(session.formdata, "genre")>
		<cfif isArray(session.formdata.genre)>
			<cfset form.genre = arrayToList(session.formdata.genre)>
		<cfelse>
			<cfset form.genre = session.formdata.genre>
		</cfif>
	<cfelse>
		<cfset form.genre = "">
	</cfif>
<cfelse>
	<cfparam name="form.name" default="">
	<cfparam name="form.email" default="">
	<cfparam name="form.genre" default="">
</cfif>

<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" />
<meta name="description" content="" />
<meta name="keywords" content="" />

&lt;link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css"&gt;    
&lt;!--[if lt IE 9]&gt;&lt;script src="http://html5shim.googlecode.com/svn/trunk/html5.js"&gt;&lt;/script&gt;&lt;![endif]--&gt;    
&lt;script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"&gt;&lt;/script&gt;    
&lt;script type="text/javascript"&gt;    
	$(function() {    

		//listen for changes to the form
		$("input").change(function() {
			console.log("A change has occurred...");
			//first grab a packet of the form
			var info = $("#theform").serializeArray();
			var jsonInfo = JSON.stringify(info);
			$.post("formsaver.cfc?method=preserve",{data:jsonInfo}, function () { });
		});			    
	});	    
&lt;/script&gt;    

</head>
<body>

&lt;div class="container"&gt;    

	&lt;form method="post" id="theform"&gt;
    &lt;fieldset&gt;
      &lt;legend&gt;Registration Form&lt;/legend&gt;

	  &lt;cfoutput&gt;
      &lt;div class="clearfix"&gt;
        &lt;label for="name"&gt;Name&lt;/label&gt;
        &lt;div class="input"&gt;
          &lt;input class="xlarge" id="name" name="name" size="30" type="text" value="#form.name#" /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
	  
      &lt;div class="clearfix"&gt;
        &lt;label for="email"&gt;Email&lt;/label&gt;
        &lt;div class="input"&gt;
          &lt;input class="xlarge" id="email" name="email" size="30" type="email" value="#form.email#" /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
	  
	  &lt;div class="clearfix"&gt;
        &lt;label id="genrelist"&gt;Genres&lt;/label&gt;
        &lt;div class="input"&gt;
          &lt;ul class="inputs-list"&gt;
            &lt;li&gt;
            	&lt;label&gt;
            	&lt;input type="checkbox" name="genre" value="scifi" &lt;cfif listFindNoCase(form.genre,"scifi")&gt;checked&lt;/cfif&gt; /&gt;
                &lt;span&gt;Science Fiction&lt;/span&gt;
				&lt;/label&gt;
			&lt;/li&gt;
            &lt;li&gt;
            	&lt;label&gt;
            	&lt;input type="checkbox" name="genre" value="horror" &lt;cfif listFindNoCase(form.genre,"horror")&gt;checked&lt;/cfif&gt; /&gt;
                &lt;span&gt;Horror&lt;/span&gt;
				&lt;/label&gt;
			&lt;/li&gt;
            &lt;li&gt;
            	&lt;label&gt;
            	&lt;input type="checkbox" name="genre" value="fantasy" &lt;cfif listFindNoCase(form.genre,"fantasy")&gt;checked&lt;/cfif&gt; /&gt;
                &lt;span&gt;Fantasy&lt;/span&gt;
				&lt;/label&gt;
			&lt;/li&gt;
            &lt;li&gt;
            	&lt;label&gt;
            	&lt;input type="checkbox" name="genre" value="crime" &lt;cfif listFindNoCase(form.genre,"crime")&gt;checked&lt;/cfif&gt; /&gt;
                &lt;span&gt;Crime&lt;/span&gt;
				&lt;/label&gt;
			&lt;/li&gt;
		  &lt;/ul&gt;
        &lt;/div&gt;
	    &lt;/cfoutput&gt;
      &lt;/div&gt;
	   &lt;div class="actions"&gt;
        &lt;input type="submit" class="btn primary" name="save" value="Save changes"&gt;&nbsp;&lt;button type="reset" class="btn"&gt;Cancel&lt;/button&gt;
      &lt;/div&gt;
	&lt;/fieldset&gt;
	&lt;/form&gt;

&lt;/div&gt;    

</body>
</html>

<cfdump var="#session#" label="For Testing..." expand="false">

Let's talk about the bottom half of this template first. The very last line is simply a debugging tag, cfdump. Since I'm storing information in the Session scope, I wanted a quick way to actually look at it while testing. I decided to keep it in the demo that you guys will play with. Above this is the form. Nothing special here, but do note the use of ColdFusion values for the form fields. This is mostly just simple values, but for the Genre field we check against a list of data.

Move up a bit into the JavaScript. I've added an input event handler for change events. My form only has inputs, but if it had a select or textarea I could add it in there as well. I make use of a jQuery utility, serializeArray, that converts all the form data into a nice little array. Arrays can't be passed as is - so I then convert that to JSON and pass it to ColdFusion.

Finally, the top portion of the code is what's handling the initial defaults. It's a bit more verbose then I'd like, but essentially we look into the Session scope for our values, and if there, set our defaults. Real quick, let's look at the CFC that handles storage.


component {


	remote void function preserve(string data) {
		if(!isJSON(arguments.data)) return;
		arguments.data = deserializeJSON(arguments.data);

		//convert the array into a name based struct
		var s = {};
		for(var i=1; i<=arrayLen(arguments.data); i++) {
			var name = arguments.data[i].name;
			if(!structKeyExists(s, name)) {
				s[name] = arguments.data[i].value;	
			} else {
				//convert into an array
				if(!isArray(s[name])) {
					s[name]	= [s[name]];
				}
				arrayAppend(s[name], arguments.data[i].value);
			}	
		}
		session.formdata = s;	
		
	}


}

Initially I had something much simpler - convert from JSON to native data and store. It was 2 lines of logic. But the array is a bit hard to deal with on the flip side. We have N items since our user may fill out one or more genre fields. Converting to a structure allows me to have a nicer way to access the data. The only "weird" part perhaps is the logic that says, "If I store more than one value for a key, turn it into an array." I could have used a List, but didn't want to assume commas wouldn't exist.

Demo this version here: http://www.raymondcamden.com/demos/2012/jan/10/draft1/form.cfm

Ok - that's draft 1. How about a version that requires no server (for persistence) but instead makes use of SessionStorage? (In case folks don't know, SessionStorage is the version of LocalStorage that doesn't last. Think of LocalStorage as fruitcake and SessionStorage as milk.) I've reduced this form just to one file now.


<cfparam name="form.name" default="">
<cfparam name="form.email" default="">
<cfparam name="form.genre" default="">

<!DOCTYPE html>    
<html>    
<head>    
    <title></title>    
	<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" />	    
	<meta name="description" content="" />    
	<meta name="keywords" content="" />    

	<link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">    
	<!--[if lt IE 9]><script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->    
	<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>    
	<script type="text/javascript">    
		$(function() {    

			//check for savedform only one
			if (sessionStorage["savedform"]) {
				console.log("yes, have something in the session to load...");
				var data = JSON.parse(sessionStorage["savedform"]);
				console.dir(data);
				for (var i = 0; i < data.length; i++) {
					if(data[i].name == "name") $("#name").val(data[i].value);
					if(data[i].name == "email") $("#email").val(data[i].value);
					if (data[i].name == "genre") {
						var checked = data[i].value;
						$("input[value='"+checked+"']").attr("checked","checked");
					}
				}
				sessionStorage.removeItem("savedform");
			}
			
			//listen for changes to the form
			$("input").change(function() {
				console.log("A change has occurred...");
				//first grab a packet of the form
				var info = $("#theform").serializeArray();				
				var jsonInfo = JSON.stringify(info);
				sessionStorage["savedform"] = jsonInfo;
			});			    
		});	    
	</script>    
</head>    
<body>    

	<div class="container">    

		<form method="post" id="theform">
        <fieldset>
          <legend>Registration Form</legend>

		  <cfoutput>
          <div class="clearfix">
            <label for="name">Name</label>
            <div class="input">
              <input class="xlarge" id="name" name="name" size="30" type="text" value="#form.name#" />
            </div>
          </div>
		  
          <div class="clearfix">
            <label for="email">Email</label>
            <div class="input">
              <input class="xlarge" id="email" name="email" size="30" type="email" value="#form.email#" />
            </div>
          </div>
		  
		  <div class="clearfix">
            <label id="genrelist">Genres</label>
            <div class="input">
              <ul class="inputs-list">
                <li>
                	<label>
                	<input type="checkbox" name="genre" value="scifi" <cfif listFindNoCase(form.genre,"scifi")>checked</cfif> />
                    <span>Science Fiction</span>
					</label>
				</li>
                <li>
                	<label>
                	<input type="checkbox" name="genre" value="horror" <cfif listFindNoCase(form.genre,"horror")>checked</cfif> />
                    <span>Horror</span>
					</label>
				</li>
                <li>
                	<label>
                	<input type="checkbox" name="genre" value="fantasy" <cfif listFindNoCase(form.genre,"fantasy")>checked</cfif> />
                    <span>Fantasy</span>
					</label>
				</li>
                <li>
                	<label>
                	<input type="checkbox" name="genre" value="crime" <cfif listFindNoCase(form.genre,"crime")>checked</cfif> />
                    <span>Crime</span>
					</label>
				</li>
			  </ul>
            </div>
		    </cfoutput>
          </div>
		   <div class="actions">
            <input type="submit" class="btn primary" name="save" value="Save changes"> <button type="reset" class="btn">Cancel</button>
          </div>
		</fieldset>
		</form>

	</div>    

</body>    
</html>

The primary area you want to look at is the code block. Let's look first at the second section - our event handler. It's been modified now to simply store the JSON string into sessionStorage. In theory I should check to see if that exists, but almost all browsers support local/sessionStorage, even IE. Yes, even IE. And shoot, if IE supports a feature you almost have to use it.

Now scroll up a bit. The block of code there will run when the page loads (and the DOM is ready). If we see a sessionStorage value, we then grab it, convert it, and update our form. I'm not terribly happy with the code in the loop there as it's very specific. You would have to update this for another form, but I'm not building a plugin here so I'm ok with hard coded values.

You can demo this version here: http://www.raymondcamden.com/demos/2012/jan/10/draft2/form.cfm

So - which version is better? The HTML5 one, right?

Well, it definitely has in it's favor less server side code. Disabling JavaScript would break both versions so that's not something that leads us to one solution or another. It's also going to require less network activity. But we've also put the work of storage into the hands of the client too, whereas before we had a 'fire and forget' logic for storage. Not to be creepy, but the first version could also be used to persist and be used for tracking and analysis.

So - thoughts? (Note - you can get all the code by clicking the download link below.)

Download attached file.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Brian Lang posted on 1/11/2012 at 4:28 AM

A couple of comments:
1. It's important to note that SessionStorage in the web browser is NOT in any way, shape, or form, related to ColdFusion Sessions. SessionStorage lasts until all web browser windows are closed.

2. I usually start a "defaults" section of code (like in your first code example) with some initialization of the fields in question - usually to blank or zero fields/variables (as appropriate). Then I do my tests to see if I have data to work with or not. You wouldn't need quite as many lines of code in this case.

Comment 2 by Raymond Camden posted on 1/11/2012 at 4:33 AM

1) I had assumed folks knew that when I mentioned localeStorage, but I good point to remind folks.

Comment 3 by Brian Lang posted on 1/11/2012 at 4:37 AM

Well, whenever I need help with a Coldfusion problem, I turn to my trusty friend Google. Usually, one of the top hits is for this blog - which almost always has enough of the answer I'm looking for to get me where I'm going.

I'm sure I'm not the only one who's encountered this phenomenon, so I figured it best to point it out for any HTML5 newbies who come across this page :)

Comment 4 by Michael Wilson posted on 1/12/2012 at 1:32 AM

I am not sure how the HTML5 version would solve the problems of the browser crashing or accidentally closing the tab. If the data stored in the SessionStorage is removed when the browser is closed, then wouldn't it be gone on the next visit to the page?

Comment 5 by Raymond Camden posted on 1/12/2012 at 1:35 AM

Good point Michael. I was thinking of a ColdFusion session that is time based. I believe you are right in that session storage would go away if the browser closes. Let me verify that though.

Comment 6 by Raymond Camden posted on 1/12/2012 at 1:38 AM

I stand corrected - thank you Michael. So this would only help if you were to click away by accident. (And I'll admit to having done that. ;)

Comment 7 by Michael Wilson posted on 1/12/2012 at 4:40 AM

I see a lot of good uses for the SessionStorage, but would local storage be the only HTML5 client solution for protecting against crashes and accidental closure?

Comment 8 by Raymond Camden posted on 1/12/2012 at 4:45 AM

Yeah - localeStorage is more permanent.

Comment 9 by Michael Ray posted on 7/2/2018 at 2:27 PM

Can you fix your links for demos and zip? Agree that localStorage would be preferred for persistent storage.

Comment 10 (In reply to #9) by Raymond Camden posted on 7/2/2018 at 4:17 PM

Demo links are removed - I don't host ColdFusion anymore. Download link is fixed.