As a blogger, one of the things I'm well aware of is how unstable a browser can be. You probably don't think of how often your browser crashes (and to be fair, it's probably been a good month since mine crashed and I run a bleeding edge Chrome install), but if you were ever 500 or so words into a blog entry (or forum posting, or comment) and your browser crashes - you become pretty aware of how nice Word's Auto Save feature is. I first blogged about this back in 2007 (Saving data in case of brower crashes). Back then I used cookies to store a temporary copy of form data while the user was editing. I thought it might be nice to update the idea using HTML 5's Local Storage feature.
HTML Local Storage is one of those darn nice HTML features that doesn't get as much press as it deserves. Unlike it's sexier cousin Canvas, though, Local Storage is actually useful. (Yes, I said it. Canvas is the hot cheerleader in high school. It's getting a lot of attention now but in five years it's going to be pregnant with it's 3rd child and working at Walmart.) I won't go into detail on how it works here (check out the excellent tutorial over at Dive Into HTML5).
The minimum you need to know that is you can treat the local storage feature like a persistent scope within JavaScript. Setting localStorage[somekey] = somevlaue is enough to store the value on the browser. There's size limits of course but for our values (strings) we won't come close to the limit. Best of all, we won't run into the "Evil Too Many Cookies" error. I once spent 2 days diagnosing an issue on a site that came down to the server simply setting one too many cookies. Did the browser bother to tell me? Of course not. It just silently ignored the cookie. (Which to be fair is fine for 99% of the world out there, but for those of who develop web sites, that type of warning would be useful. Unlike Canvas. Ok I'll stop now.)
My basic plan of attack will be this:
- When the browser loads, look for values in the local storage object and restore them.
- Every N seconds, store a copy.
- When the form posts, clear the local storage. (We assume nothing goes wrong server side. Maybe not a safe assumption, but I'm going with it for now.)
Before we do anything, we need to ensure our browser supports local storage. For that, I'm taking a nice function right from the Dive into HTML site:
//Credit: http://diveintohtml5.org/storage.html
function supports_html5_storage() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
}
The first thing we do is check for existing values. This will be done when the page loads so I've got it inside my $(document) block:
$(document).ready(function() {
if (supports_html5_storage()) {
//load in old data
loadOld();
That's the loading snipping, and here's loadOld (which is a horrible name, sorry):
function loadOld() {
console.log("Running loadOld");
if(localStorage["blogentry.title"]) $("#title").val(localStorage["blogentry.title"]);
if(localStorage["blogentry.abstract"]) $("#abstract").val(localStorage["blogentry.abstract"]);
if(localStorage["blogentry.body"]) $("#body").val(localStorage["blogentry.body"]);
if(localStorage["blogentry.category"]) $("#category").val(localStorage["blogentry.category"]);
}
Each of these lines are exactly the same except for the key and DOM item they use. Notice that I'm using keys named blogentry.something. Local Storage is unique per site and my blog may be only one small part of a larger application. Using "title" would be too vague so I used a more verbose, but potentially safer naming system.
Next we need to set up our code to store the data. I've got an interval defined first:
window.setInterval(saveData, 5 * 1000);
That 5 second duration there is pretty arbitrary. I type pretty quickly so 5 seconds seems like a reasonable interval. Now for the actual function:
function saveData(){
localStorage["blogentry.title"] = $("#title").val();
localStorage["blogentry.abstract"] = $("#abstract").val();
localStorage["blogentry.body"] = $("#body").val();
localStorage["blogentry.category"] = $("#category").val();
}
Really complex, right? I love how simple this feature is! The final part is handling form submission. We want to clear out the values:
$("#blogEntry").submit(function() {
//could do validation here as well
localStorage["blogentry.title"] = "";
localStorage["blogentry.abstract"] = "";
localStorage["blogentry.body"] = "";
localStorage["blogentry.category"] = "";
});
Notice I'm not actually removing the value but setting it to an empty string. You can also use removeItem(key) to actually delete the entire value. An example of that would be:
localStorage.removeItem("blogentry.title");
As I expect folks to be running this code again and again, it seems ok to keep the key there with an empty value. Ok, so let's not put it all together.
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
//Credit: http://diveintohtml5.org/storage.html
function supports_html5_storage() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
} function loadOld() {
console.log("Running loadOld");
if(localStorage["blogentry.title"]) $("#title").val(localStorage["blogentry.title"]);
if(localStorage["blogentry.abstract"]) $("#abstract").val(localStorage["blogentry.abstract"]);
if(localStorage["blogentry.body"]) $("#body").val(localStorage["blogentry.body"]);
if(localStorage["blogentry.category"]) $("#category").val(localStorage["blogentry.category"]);
} function saveData(){
console.log("Running saveData");
localStorage["blogentry.title"] = $("#title").val();
localStorage["blogentry.abstract"] = $("#abstract").val();
localStorage["blogentry.body"] = $("#body").val();
localStorage["blogentry.category"] = $("#category").val();
} $(document).ready(function() {
console.log('init');
if (supports_html5_storage()) {
//load in old data
loadOld();
//start storing
window.setInterval(saveData, 5 * 1000);
//handle the form
$("#blogEntry").submit(function() {
//could do validation here as well
localStorage["blogentry.title"] = "";
localStorage["blogentry.abstract"] = "";
localStorage["blogentry.body"] = "";
localStorage["blogentry.category"] = "";
});
} });
</script>
<style>
fieldset {
width: 400px;
margin-bottom: 10px;
}
input[type=text] {
width: 100%;
}
textarea {
width: 100%;
height: 50px;
}
</style>
</head>
<body> <form id="blogEntry" method="post"> <fieldset>
<label for="title">Title</label>
<input type="text" name="title" id="title">
</fieldset> <fieldset>
<label for="abstract">Abstract</label>
<textarea name="abstract" id="abstract"></textarea>
</fieldset> <fieldset>
<label for="body">Body</label>
<textarea name="body" id="body"></textarea>
</fieldset> <fieldset>
<label for="category">Category</label>
<select name="category" id="category">
<option value="1">ColdFusion</option>
<option value="2">jQuery</option>
<option value="3">Flex</option>
<option value="4">Star Wars</option>
</select>
</fieldset> <input type="submit"> </form> </body>
</html>
<html>
Check out the demo of this here. And before you ask - yes - Local Storage does work in IE. However I used console messages while testing. If you want to try this on IE, just copy and paste the code and remove the console statements.
Archived Comments
Nice tutorial!
"Canvas is the hot cheerleader in high school. It's getting a lot of attention now but in five years it's going to be pregnant with it's 3rd child and working at Walmart.)"
YMMD! :)
Rock on. I like a lot. This is really easy to implement. The only additional thing I would probably implement in production is to actually check with the user if they want to use the saved info. Which is also extremely easy to add in there.
Good point. My first inclination would be to use a jQuery UI dialog, but really, the simple, and super old, confirm() prompt would probably work best.
Cool idea! Something I'd like to add, though: in an actual blog or CMS, there's good chance that users will be editing more than one page at the same time, so the naming scheme should probably take that into account. Otherwise they can get into edit wars with themselves.
Good point. I'd probably make a simple utility called getPrefix() for the page that handled that and returned a root key. You would then just do:
localStorage[getPrefix() + "title"] = ...
Tried this in Firefox in private browsing mode. Interesting that the DOM will indicate that the private storage function exists, so your saveall function runs, _but_ the data doesn't survive a browser refresh or restart. Probably not a big deal, but just something to tuck back in a brain space somewhere so if someone were to report it didn't work...
I'd consider that a bug. At minimum it should have thrown an error.
Oh - and that goes right to one of my biggest pet peeves about browsers not being more verbose. For the general public it is fine, but for us web developers, Firefox really should have said _something_ when it ignored the call.
"...browsers not being more verbose..." - Could not agree more.
In a perfect world, it should have responded back (in private browsing mode) with a better status -- like "don't depend on me cause I'm going to puke all this data as soon as you're not looking" ... or maybe "2". [shrug]
Granted, the storage exists for the session, and that's moderately useful.
Still I wonder if there's a potential check that could be made to see if data WOULD survive -- I can sort of feel around what it would be like -- maybe like "&& if (canPersistData)", which would be useful for cookie handling as well -- would be good to know that "O, BTW, those cookies you just set and are sort of depending on? Yea, not so much".
Maybe the 5 spec has something...
Dumb question - and I can test this too, but if you set localStorage["test"] = 2, and then check the value, is it null or 2? If null, then you can make that a simple test function.
The answer is, of course, it depends.
(LDS = local dfata sorage, PBM=Private Browsing Mode)
I just sped-read the specs, and rationale behind PBM and LDS
One doc from WHATWG talked to implementation of this and the different philosophies to take. Some said, when entering PBM to take a copy of existing data with it, but not save changes at session end. Some said set quota for LDS to 0, which would effectively not allow any storage, and some said establish new pool for LDS, but dump at end.
Each browser vendor that implemented preferred a differing approach (figures). Therefore, some might return 2, and some might return null.
That kinda sucks. But I think returning null would be a bad thing because:
1) "they" from a security aspect saw a vulnerability in identifying that the browser was in PBM -- idea being that client ins in PBM for a reason, and the website should never know because it might try to work around that to attack from a different vector. I understand and appreciate that point of view.
2) LDS is alterable by the user -- apparently the Chrome version of Angry Birds was hacked that way -- the award of stars was changed by modifying the LDS to provide moar stars. Makes sense -- even cookies are alterable.
I suppose as long as you're not storing anything mission critical (or if you are, but can test for validity before you use), LDS isn't a bad thing -- and your use case is a good example -- giving the user an extra life, so to speak, but not depending on the it...
(last note. I loved mainframe days. We had more control... :) )
Hmm, I see your point about hiding the fact that you are in PBM.
Thanks for digging into this!
thank you!!!!