Advertisement

Real-World Off-Line Data Storage

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

In many projects there comes a time when you'll need to store some data off-line. It may be a requirement or just an improvement for your users, but you have to decide which of the available storage options you will use in your application. This article will help you choose the best one, for your app.


Introduction

HTML5 introduced a few off-line storage options. AppCache, localStorage, sessionStorage and IndexedDB. Every one of them is suitable for a specific use. For example, AppCache can boost your application or let some parts of it work without an Internet connection. Below, I will describe all of these options and show a few code snippets with example usage.


AppCache

If a part of your application (or the whole app) can be used without access to the server, you can use AppCache to enable your users to do some things off-line. All you need to do is to create a manifest file where you would specify what should be cached and what shouldn't be. You can also specify replacements for the files that require on-line access.

An AppCache manifest is just a text file with a .appcache (recommended) extension. It starts with CACHE MANIFEST and is divided in to three parts:

  • CACHE - files you specify here will be downloaded and cached the first time the user accesses your site
  • NETWORK - here you list the files that require an Internet connection to work properly, they will never be cached
  • FALLBACK - these files will be used when an on-line resource is accessed without a connection

Example

First, you have to define the manifest file on your page:

<!DOCTYPE html>
<html manifest="manifest.appcache">
...
</html>

You need to remember that the manifest file must be served with a text/cache-manifest MIME-type, otherwise it will not be parsed by the browser. Next, you need to create the file you defined earlier. For the purpose of this example, let's imagine that you have an informational website with the ability to contact you and write comments. You can let users access the static parts of the site and replace the contact form and comments with other information so that the form and comments are inaccessible while off-line.

First, let's define some static content:

CACHE MANIFEST

CACHE:
/about.html
/portfolio.html
/portfolio_gallery/image_1.jpg
/portfolio_gallery/image_2.jpg
/info.html
/style.css
/main.js
/jquery.min.js

Side Note: one bad thing about the manifest is that you can't use a wildcard sign to indicate that, for example, a whole folder should be cached, you can only use a wildcard under the NETWORK section to indicate that all resources not listed in the manifest should not be cached.

You don't need to cache the page on which the manifest is defined, it will be cached automatically. Now we will define fallbacks for the contact and comments sections:

FALLBACK:
/contact.html /offline.html
/comments.html /offline.html

Finally, we can use an * to stop all other resources from being cached:

NETWORK:
*

The final result should look like this:

CACHE MANIFEST

CACHE:
/about.html
/portfolio.html
/portfolio_gallery/image_1.jpg
/portfolio_gallery/image_2.jpg
/info.html
/style.css
/main.js
/jquery.min.js

FALLBACK:
/contact.html /offline.html
/comments.html /offline.html

NETWORK:
*

An important thing to remember is that your resources will only be cached once. They will not get cached when you update them, only when you change the manifest. A good practice is to enter in a comment with a version number and increase it every time you update the file:

CACHE MANIFEST

# version 1

CACHE:
...

LocalStorage & SessionStorage

These two storage options will be useful if you want to preserve something in your JavaScript code. The first one lets you save a value without an expiration date. This value will be accessible for any page with the same domain and protocol. For example, you may want to save the user's application settings on his/her computer so he/she can adjust them to the computer they currently use. The second one will hold the values until the user closes the browser window (or tab). Also, the data is not shared between windows, even if the user opens a few pages of your application.

Something worth remembering is that you can store only basic types in localStorage/sessionStorage. So only strings and numbers will work. Everything else will be stored using it's toString() method. If you need to save an object, you should do it using JSON.stringify (if this object is a class, you can just override the default toString() method to do it for you automatically).

Example

Let's consider the previous example. In the comments and contact sections of the site, we can save what the user typed in, so if he/she accidentally closes the window, the values will still be there for him/her to continue later on. This will be a really simple piece of code using jQuery (since we will be using a field's id to identify it later, each of the form fields will need to have an id attribute)

$('#comments-input, .contact-field').on('keyup', function () {
	// let's check if localStorage is supported
	if (window.localStorage) {
		localStorage.setItem($(this).attr('id'), $(this).val());
	}
});

When the comment/contact form is sent, we have to clear the value. Let's do this by handling a submit event (here's the most basic example):

$('#comments-form, #contact-form').on('submit', function () {
	// get all of the fields we saved
	$('#comments-input, .contact-field').each(function () {
		// get field's id and remove it from local storage
		localStorage.removeItem($(this).attr('id'));
	});
});

And finally, on page load, we will restore the values:

// get all of the fields we saved
$('#comments-input, .contact-field').each(function () {
	// get field's id and get it's value from local storage
	var val = localStorage.getItem($(this).attr('id'));
	// if the value exists, set it
	if (val) {
		$(this).val(val);
	}
});

IndexedDB

This is the most interesting storage option in my opinion. It allows you to store rather large amounts of indexed data into the user's browser. This way, you can save complex objects, large documents, etc. and have your user access them without an Internet connection. This feature is useful for all kinds of applications - if you are making an email client, you can save the user's emails so he/she can access them later, a photo album could save photos for off-line use, or GPS navigation can save a particular route and the list goes on.

IndexedDB is an object-oriented database. This means that there are no tables and no SQL. You store key-value pairs of data, where keys are strings, numbers, dates or arrays and values can be complex objects. The database itself is composed from stores. A store is similar to a table in a relational database. Each value must have it's own key. A key can be generated automatically, you can specify it when you add the value, or it can be some field in the value (which can also be generated automatically). If you decide to use a field as a key, you will only be able to add JavaScript objects to the store (because simple numbers or strings can't have any properties like objects can).

Example

For this example, let's imagine that we have a music album. Now, I'm not going to cover building the entire music album app here. I will only be covering the IndexedDB part of the app, but the music album app itself is included with this article for you to download, so you can look at the complete source code there. First, we have to open the database and create the store:

// check if the indexedDB is supported
if (!window.indexedDB) {
	throw 'IndexedDB is not supported!'; // of course replace that with some user-friendly notification
}

// variable which will hold the database connection
var db;

// open the database
// first argument is database's name, second is it's version (I will talk about versions in a while)
var request = indexedDB.open('album', 1);

request.onerror = function (e) {
	console.log(e);
};

// this will fire when the version of the database changes
request.onupgradeneeded = function (e) {
	// e.target.result holds the connection to database
	db = e.target.result;
	
	// create a store to hold the data
	// first argument is the store's name, second is for options
	// here we specify the field that will serve as the key and also enable the automatic generation of keys with autoIncrement
	var objectStore = db.createObjectStore('cds', { keyPath: 'id', autoIncrement: true });
	
	// create an index to search cds by title
	// first argument is the index's name, second is the field in the value
	// in the last argument we specify other options, here we only state that the index is unique, because there can be only one album with specific title
	objectStore.createIndex('title', 'title', { unique: true });
	
	// create an index to search cds by band
	// this one is not unique, since one band can have several albums
	objectStore.createIndex('band', 'band', { unique: false });
};

The above code is pretty simple. You probably noticed the version and the onupgradeneeded event. This event is fired when the database is opened with a new version. Since the database didn't exist yet, the event fires and we can create the store we need. Later we add two indexes, one to search by title and one to search by band. Now let's see the process of adding and removing albums:

// adding
$('#add-album').on('click', function () {
	// create the transaction
	// first argument is a list of stores that will be used, second specifies the flag
	// since we want to add something we need write access, so we use readwrite flag
	var transaction = db.transaction([ 'cds' ], 'readwrite');
	transaction.onerror = function (e) {
		console.log(e);
	};
	var value = { ... }; // read from DOM
	// add the album to the store
	var request = transaction.objectStore('cds').add(value);
	request.onsuccess = function (e) {
		// add the album to the UI, e.target.result is a key of the item that was added
	};
});

// removing
$('.remove-album').on('click', function () {
	var transaction = db.transaction([ 'cds' ], 'readwrite');
	var request = transaction.objectStore('cds').delete(/* some id got from DOM, converted to integer */);
	request.onsuccess = function () {
		// remove the album from UI
	}
});

Pretty straightforward. You need to remember that all operations on the database are based on transactions to preserve consistency of data. Now the only thing left to do is to display the albums:

request.onsuccess = function (e) {
	if (!db) db = e.target.result;
	
	var transaction = db.transaction([ 'cds' ]); // no flag since we are only reading
	var store = transaction.objectStore('cds');
	// open a cursor, which will get all the items from database
	store.openCursor().onsuccess = function (e) {
		var cursor = e.target.result;
		if (cursor) {
			var value = cursor.value;
			$('#albums-list tbody').append('<tr><td>'+ value.title +'</td><td>'+ value.band +'</td><td>'+ value.genre +'</td><td>'+ value.year +'</td></tr>');

			// move to the next item in the cursor
			cursor.continue();
		}
	};
}

This is also not very complicated. As you can see, using IndexedDB you can store complex values really easily. You can also search for values by index, like this:

function getAlbumByBand(band) {
	var transaction = db.transaction([ 'cds' ]);
	var store = transaction.objectStore('cds');
	var index = store.index('band');
	// open a cursor to get only albums with specified band
	// notice the argument passed to openCursor()
	index.openCursor(IDBKeyRange.only(band)).onsuccess = function (e) {
		var cursor = e.target.result;
		if (cursor) {
			// render the album
			// move to the next item in the cursor
			cursor.continue();
		}
	});
}

You can use the cursor with the index just like how we did with the store. Since there may be a few entries with the same index value (if it's not unique) we need to use IDBKeyRange. This will filter the results depending on what function you use. Here, we want to only get items by the provided band, so we used the only() method. You can also use lowerBound(), upperBound() and bound. The method names are pretty self explanatory.


Conclusion

So enabling off-line access for your users is not as complicated as it may seem. I hope that after reading this article you will make your applications more user-friendly by allowing them to access some parts (or maybe even all) of it without an Internet connection. You can download the sample app and experiment with it, adding more options or including some parts of it into your website.

Advertisement