Saving Form Data in Client-side Storage

Today’s post is one of those that started off with me worrying that it was going to be too simple and quickly turned into a bit of a complex little beast. I love that as it usually means my expectations were wrong and I’ve got a chance to expand my knowledge a bit. This post came from a simple idea: While working on a form, we can save your form data for restoring later in case you navigate away, close the tab by accident, or perhaps get “surprised” by an operating system update. While this is not something you would want to use in every situation (for example, storing a new password field), there are plenty of examples where this could be helpful, especially in a larger form.

For our demo, I will only cover client-side storage, which means the data will be unique to one browser on the device. Although what I described here could be tied to a back-end service for storing temporary form data as well.

Initially, I tried to build a generic solution that would apply to any and all forms but quickly discovered that it became Non-Trivial to the point where I decided a more hard-coded solution would be better. My assumption here is that my readers can take these techniques and apply them to their site with a bit of work. As always, if you have any questions, just let me know!

The Form

Alright, let’s start off by looking at the form I’ll use for the demo. While covering every unique kind of form field would be overwhelming, I tried to cover the main ones: A few text fields, a set of checkboxes, a set of radio fields, and a text area. Here’s the HTML:

<form id="mainForm">
	<p>
	<label for="name">Name</label>
	<input type="text" name="name" id="name">
	</p>
	<p>
	<label for="email">Email</label>
	<input type="email" name="email" id="email">
	</p>
	<p>
	<label for="inus">In US?</label>
	<select name="inus" id="inus">
		<option></option>
		<option value="true">Yes</option>
		<option value="false">No</option>
	</select>
	</p>
	<p>
		<label for="department">Department</label><br/>
		<input type="radio" name="department" id="dept1" value="dept1"><label for="dept1">Dept 1</label><br/>
		<input type="radio" name="department" id="dept2" value="dept2"><label for="dept2">Dept 2</label><br/>
		<input type="radio" name="department" id="dept3" value="dept3"><label for="dept3">Dept 3</label><br/>
</p>
<p>
	<label for="cookie">Favorite Cookie (Select as many as you want):</label><br/>
	<input type="checkbox" name="cookie" id="cookie1" value="Chocolate Chip"><label for="cookie1">Chocolate Chip</label><br/>
	<input type="checkbox" name="cookie" id="cookie2" value="Sugar"><label for="cookie2">Sugar</label><br/>
	<input type="checkbox" name="cookie" id="cookie3" value="Ginger"><label for="cookie3">Ginger</label><br/>
	<input type="checkbox" name="cookie" id="cookie4" value="BW"><label for="cookie4">Black & White</label><br/>
</p>		
<p>
	<label for="comments">Comments</label><br/>
	<textarea name="comments" id="comments"></textarea>
</p>
<p>
	<input type="submit">
</p>
</form>

It’s not terribly exciting but gets the job done in terms of demonstrating multiple types of form fields.

html form

Saving Form Data

Let’s begin with how to save the form. Here’s the high-level approach I’m going to use:

  • The data will be stored in LocalStorage. This will let it persist forever (not really, but close enough) and will be an incredibly simple API to work with. IndexedDB can store a lot more data, but all we’re storing is a form.
  • I will persist the data on every change. We could get fancy and save on an, but it’s relatively inexpensive to just save on every change.

To begin, I set up my code to fire some logic on DOMContentLoaded as well as create some global variables:

document.addEventListener('DOMContentLoaded',init,false);

let name, email, inus, depts, cookies, comments;

Now let’s look at init:

function init() {
	// get the dom objects one time
	name = document.querySelector('#name');
	email = document.querySelector('#email');
	inus = document.querySelector('#inus');
	depts = document.querySelectorAll('input[name=department]');
	cookies = document.querySelectorAll('input[name=cookie]');
	comments = document.querySelector('#comments');
	
	// listen for input on all
	let elems = Array.from(document.querySelectorAll('#mainForm input, #mainForm select, #mainForm textarea'));
	elems.forEach(e => e.addEventListener('input', handleChange, false));
}

As I mentioned above, I’m not going for a generic solution, but rather one tied to my exact form. You can see then I create a variable representing the DOM item for each of my fields. depts and cookies ae special as they are a set of items, not just one.

But while I’m not going dynamic to set up the variables pointing to the form fields, I did go dynamic to set up the event handler. I could have added an event listener for each of my variables (while ensuring I handled depts and cookies in a loop), but this shortcut handles matching any form fields inside my form and then letting me quickly assign the handler for each.

Now that we’ve got event handlers, we can build logic to persist the form. This handler will fire on any change in the fields, but as I said above, we’ll get all the data and persist.

function handleChange(e) {
	
	console.log('handleChange');
	/*
	get all values and store
	first the easy ones
	*/
	let form = {};
	form.name = name.value;
	form.email = email.value;
	form.inus = inus.value;
	form.comments = comments.value;
	// either null or one
	depts.forEach(d => {
		if(d.checked) form.department = d.value;
	});
	// either empty array or some things
	form.cookies = [];
	cookies.forEach(c => {
		if(c.checked) form.cookies.push(c.value);
	});
	
	// now store
	saveForm(form);
}

I create an object, formto store my data, and then get the “simple” ones where I can just check the value. This works for the select tag too. For the radio and checkbox ones, I handle them a bit differently. The radio one, deptswill either have nothing selected or one, so if nothing is picked, it’s never saved, or it’s a value. For cookiesI’ll always have an empty array at a minimum, but will fill it with the values ​​when selected.

Finally, I take the data and pass it to another function. The code to use LocalStorage is very simple, but I wanted it abstracted in case the decision was made to change to something else in the future. Here’s that function:

function saveForm(form) {
	let f = JSON.stringify(form);
	window.localStorage.setItem('form', f);
}

Remember that LocalStorage only takes simple values, so the object is serialized to a string first.

Woot! Ok, at this point, I can type in data, and confirm it’s working in DevTools:
Deftools output

Retrieving the Data

Now that there’s a way to store the form, let’s look at fetching the data (if it exists) and using it. Back in initI added the following:

// do we have a cached form?
let cached = getForm();
if(cached) {
	name.value = cached.name;
	email.value = cached.email;
	inus.value = cached.inus;
	comments.value = cached.comments;
	if(cached.department) {
		depts.forEach(d => {
			if(d.value === cached.department) d.checked = true;
		});
	}
	if(cached.cookies) {
		cookies.forEach(c => {
			if(cached.cookies.includes(c.value)) c.checked = true;
		});
	}
}

I begin by fetching the form (I’ll show that in a second) and if the cache exists, I make use of it. All the simple values ​​and the select, it’s easy to set. For the checkbox and radio ones, it’s slightly more complex. depts will either be null or a value, but cookies will be an array (technically it will always exist so if isn’t really necessary) and I make use includes to check the cached array.

As with saveForm, I wanted to wrap the cache retrieval logic to handle updating the storage in the future. Here’s getForm:

function getForm() {
	let f = window.localStorage.getItem('form');
	if(f) return JSON.parse(f);
}

There’s one last thing to do. When the form is submitted, it makes sense to clear the cache. I added this to the end of init:

document.querySelector('#mainForm').addEventListener('submit', () => {
	window.localStorage.removeItem('form');
}, false);

This is nice and simple, but I’m being a bit inconsistent here by not abstracting out how I work with persistence. It’s one line, and I feel kinda bad about it, but I’m also fine leaving it for now.

Here’s the complete demo for you to play with: Demo

.

Leave a Comment