Saving Form Data in Client-side Storage
A look at using vanilla JavaScript to automatically store and cache form data as a user enters information.
Join the DZone community and get the full member experience.
Join For FreeToday'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, can we 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.
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 interval, 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, form
to 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, depts
will either have nothing selected or one, so if nothing is picked, it's never saved, or it's a value. For cookies
, I'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:
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 init
, I 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
Published at DZone with permission of Raymond Camden, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments