Category Archives: web apps

Saving form input when you’re offline using localStorage

Leading on from the work I did with the application cache, I became interested in using local storage to allow me to input data into a form and save it offline, ready for synching later on when I was online.

I’ve made use of JSON.js for handling JSON and jQuery in the code below for some convenience, you should be able to swap out for your library of choice.

Some caveats:

  • potential for data collisions if you edit on another device or browser between saving offline and synching.
    • you could feasibly solve this by dynamically building duplicate form fields that sit next to the server version and contain the local data.  A button to copy across from one field to another is quite a simple trick.
  • you get 5MB of localstorage per domain, meaning it isn’t suitable for documents over 5000 characters (including input names and syntax) or for saving a vast amount of forms without synchronising. There’s a warning provided but it happens at a stage where the storage is already refusing to save, so the user has to copy their content to another file
  • local storage only handles text in key-value strings, so you can’t save your data as a JSON object, you have to stringify it.
  • Try…Catch is not my favourite way of handling detection or errors, but in the 2 cases below it is the only practical option

Inspiration:


/*
OnOffApp makes use of local storage to save form input locally when a user is offline and allow them to synchronise it when they are back online again.
Dependencies:	json.js http://www.json.org/
				jQuery http://www.jquery.com/
Config:			when calling init, there are 5 items that can be optionally configured
				id
					unique identifier for the form to be used in local storage as the key
				field_id
					jQuery object of the field that supplies the id, used to find the parent form, otherwise form defaults to all forms on page
				synchMsg
					HTML to display to inform the user they have items to be synchronised to the server
				synchBtn
					HTML for synchronising button
				warnMsg
					HTML for the message to show the user when local storage is full and data cannot be saved
*/

OnOffApp = (function(){
	
	//variables available throughout the function
	var UID;
	var postForm = $("form");
	var holder = $("<div id=\"synchMsg\"></div>");
	var config = {};
	
	//initialiser function
	//@argument usrconfig: an object of config options
	var init = function(usrconfig){
		setup_config(usrconfig);
		var submit_button = postForm.delegate("input[type=submit]", "click", submit_click);
		if(retrieveLocalData() === true){
			synchMessage();
		}
		$(window).bind("online", synchMessage);
		$(window).bind("offline", synchMessage_remove);
	};
	
	//set-up defaults for user configurable messages and overwrite where specified
	//@arguments usrconfig: object of user-set options
	//@returns the config object
	var setup_config = function(usrconfig){
		var id = location.href;
		config.synchMsg = "<p class=\"synch\">Locally saved data can be synchronised now</p>";
		config.synchBtn = "<input type=\"submit\" value=\"Synch\" class=\"synch\" />";
		config.warnMsg = "<p class=\"warn\">Local storage is full.</p>";
		 for(var i in usrconfig){
			if(i === "id"){
				id = usrconfig[i].val();
				continue;
			}
			else if(i === "field_id"){
				postForm = usrconfig[i].closest("form");
				continue;
			}
			config[i] = usrconfig[i];
		 }
		 UID = "uID-" + id;
		 return config;
	};
	
	//submit button clicked
	//detect if we are online.  If so, grab any saved data as well as existing data and save via AJAX, if not save to local storage
	var submit_click = function(e){
		e.preventDefault();
		var data = getFormInput();
		var savedData = $.parseJSON(getLocalData());
		if(navigator.onLine){
			saveToServer($.extend({},savedData,data));
		}
		else{
			setLocalData(JSON.stringify(data));
		}
	};
	
	//get the form inputs and put into object notation
	//@returns an object
	var getFormInput = function(){
		var formArray = postForm.serializeArray(),
			o = {};
			$.each(formArray, function(){
				o[this.name] = this.value;
			});
		return o;
	};
	
	//get localStorage data
	//@returns string of locally stored data or an empty string
	var getLocalData = function(){
		return localStorage.getItem(UID) || "";
	};
	
	//set localStorage data, if out of space add a warning instead
	var setLocalData = function(data){
		try {
			localStorage.setItem(UID, data);
		}
		catch (e) {
			if (e == QUOTA_EXCEEDED_ERR) {
				postForm.prepend(config.warnMsg);
			}
		}
	};
	
	//get the data from localStorage and add saved values into the relevant fields
	//@returns true or false depending on whether there was locally stored data
	var retrieveLocalData = function(){
		var oldData = getLocalData();
		if(oldData !== ""){
			var fields = $.parseJSON(oldData),
				field;
			$.each(fields, function(key, val){
				field = $("[name=" + key + "]");
				if(field.length > 0){
					if(field.is(":checkbox") || field.is(":radio")){
						field.attr("checked", "checked");
					}
					else{
						field.val(val);
					}
				}
			});
			return true;
		}
		return false;
	};
	
	//if you have localStorage data and you are online, prompt the user to synchronise their data (i.e. save it to the server)
	var synchMessage = function(){
		if(navigator.onLine && getLocalData() !== ""){
			if($("#synchMsg").length === 0){
				holder.append(config.synchMsg).append(config.synchBtn);
				postForm.prepend(holder);
			}
			else{
				holder.show();
			}
		}
	};
	
	//remove the synch message
	var synchMessage_remove = function(){
		holder.hide();
	};
	
	//save form data to the server via AJAX
	var saveToServer = function(data){
		var url = postForm.attr("action");
		var params = $.param(data);
		var me = this;
		$.ajax({
			method: "post",
			url: url,
			data: params,
			success: savedSuccess(data)
		});
	};
	
	//after data saved to server, remove the saved data from localStorage and remove the synchronise message
	var savedSuccess = function(){
		localStorage.removeItem(UID);
		synchMessage_remove();
	};
	
	//interfacing methods and properties
	return {
		init: init
	};
}());


//For the purposes of the demo, there is a hidden input with id "post_id" with the unique identifier for the post (like WordPress)
$(document).ready(function(){
	try {
		if('localStorage' in window && window['localStorage'] !== null){
			OnOffApp.init({
				"id_field" : $("#post_id"),
				"id" : $("#post_id").val(),
				"warnMsg" : "<p class=\"warn\">Sorry, your local storage is full up.  Please copy and save your latest work to a text file and go online to synchronise your work as soon as you can.</p>",
				"synchMsg" : "<p class=\"synch\">You have locally saved data that you can now synchronise with the live server</p>"
			});
		}
	}
	catch(e) {
		return false;
	}
});

A better application cache manifest

I’ve recently been messing around with offline application caching.  At a simple static level it is pretty basic, but once you start thinking about what might actually happen in real life it gets a lot more ugly.

The following sources got me started with offline caching in the first place.  If you need more background to what application cache is, you should read these first.

My main problem with it is that every time I make a change to my files, I have to go into my cache.manifest file and update a comment somewhere.  Why? Because the application cache is checked for updates first.  If there are no updates, it just stops.  It doesn’t check any of the underlying files for updates.

That’s efficient in terms of client-server communication by reducing the amount of calls to check whether cached versions can be used or need updated, but its also really dumb, and a nuisance to work with.  You start relying on people to remember on these little jobs, and that introduces a risk of human error.  It’s not an acceptable risk because it is an avoidable risk.

Luckily we can use server-side technology to generate our cache manifests and grab the file update timestamp.  Here’s what I’ve learned thus far:

  1. the manifest attribute on the html element doesn’t have to point to a *.manifest file.  It can point to a manifest.php file (for example).
  2. in the dynamic page, set your header to serve up the right mime type.  In PHP, this is
    <? php header("Content-Type: text/cache-manifest;charset=utf-8"); ?>
  3. find the most recently updated file date and echo it out in a comment. If there’s not been a change, the comment will remain the same in the generated code as it is in the cached version.   In my example below I’m statically setting which files I want to be in the cache. Jonathan Stark shows a way of going through the file system to cache all files if you’d rather.  You’d just add the timestamp bits in where required.
<?php
	header("Content-Type: text/cache-manifest;charset=utf-8");
	$cache_files = array(
					"testhtml5.htm",
					"testinvisdyn.php",
					"testhtml5.css",
					"testhtml5.js",
					"jquery.js",
					"json.js"
					);
	$fallback_files = array(
						"/" => "fallback/testhtml5fall.htm"
					   );
?>
CACHE MANIFEST
NETWORK:
*
CACHE:
<?php
	$cache_mod = 0;
	foreach($cache_files as $f){
		echo "$f \n";
		$last_mod = filemtime($f);
		if($last_mod > $cache_mod){
			$cache_mod = $last_mod;
		}
	}
	unset($f);
	
	echo "# $cache_mod \n";
?>

FALLBACK:
<?php
	$cache_mod = 0;
	foreach($fallback_files as $d => $f){
		echo "$d $f \n";
		$last_mod = filemtime($f);
		if($last_mod > $cache_mod){
			$cache_mod = $last_mod;
		}
	}
	unset($f);
	unset($d);
	
	echo "# $cache_mod \n";
?>

Disclaimer: I’m not a PHP developer, its not one of my professional competencies.  If the above code sucks, please respond in the comments and I’ll fix it up.