Saving forms
I added a long-overdue enhancement to The Session recently. Here’s the scenario…
You’re on a web page with a comment form. You type your well-considered thoughts into a textarea
field. But then something happens. Maybe you accidentally navigate away from the page or maybe your network connection goes down right when you try to submit the form.
This is a textbook case for storing data locally on the user’s device …at least until it has safely been transmitted to the server. So that’s what I set about doing.
My first decision was choosing how to store the data locally. There are multiple APIs available: sessionStorage
, IndexedDB
, localStorage
. It was clear that sessionStorage
wasn’t right for this particular use case: I needed the data to be saved across browser sessions. So it was down to IndexedDB
or localStorage
. IndexedDB
is the more versatile and powerful—because it’s asynchronous—but localStorage
is nice and straightforward so I decided on that. I’m not sure if that was the right decision though.
Alright, so I’m going to store the contents of a form in localStorage
. It accepts key/value pairs. I’ll make the key the current URL. The value will be the contents of that textarea
. I can store other form fields too. Even though localStorage
technically only stores one value, that value can be a JSON object so in reality you can store multiple values with one key (just remember to parse the JSON when you retrieve it).
Now I know what I’m going to store (the textarea
contents) and how I’m going to store it (localStorage
). The next question is when should I do it?
I could play it safe and store the comment whenever the user presses a key within the textarea
. But that seems like overkill. It would be more efficient to only save when the user leaves the current page for any reason.
Alright then, I’ll use the unload
event. No! Bad Jeremy! If I use that then the browser can’t reliably add the current page to the cache it uses for faster back-forwards navigations. The page life cycle is complicated.
So beforeunload
then? Well, maybe. But modern browsers also support a pagehide
event that looks like a better option.
In either case, just adding a listener for the event could screw up the caching of the page for back-forwards navigations. I should only listen for the event if I know that I need to store the contents of the textarea
. And in order to know if the user has interacted with the textarea
, I’m back to listening for key presses again.
But wait a minute! I don’t have to listen for every key press. If the user has typed anything, that’s enough for me. I only need to listen for the first key press in the textarea
.
Handily, addEventListener
accepts an object of options. One of those options is called “once
”. If I set that to true
, then the event listener is only fired once.
So I set up a cascade of event listeners. If the user types anything into the textarea
, that fires an event listener (just once) that then adds the event listener for when the page is unloaded—and that’s when the textarea
contents are put into localStorage
.
I’ve abstracted my code into a gist. Here’s what it does:
- Cut the mustard. If this browser doesn’t support
localStorage
, bail out. - Set the
localStorage
key to be the current URL. - If there’s already an entry for the current URL, update the
textarea
with the value inlocalStorage
. - Write a function to store the contents of the
textarea
inlocalStorage
but don’t call the function yet. - The first time that a key is pressed inside the
textarea
, start listening for the page being unloaded. - When the page is being unloaded, invoke that function that stores the contents of the
textarea
inlocalStorage
. - When the form is submitted, remove the entry in
localStorage
for the current URL.
That last step isn’t something I’m doing on The Session. Instead I’m relying on getting something back from the server to indicate that the form was successfully submitted. If you can do something like that, I’d recommend that instead of listening to the form submission event. After all, something could still go wrong between the form being submitted and the data being received by the server.
Still, this bit of code is better than nothing. Remember, it’s intended as an enhancement. You should be able to drop it into any project and improve the user experience a little bit. Ideally, no one will ever notice it’s there—it’s the kind of enhancement that only kicks in when something goes wrong. A little smidgen of resilient web design. A defensive enhancement.