Setting up IndexedDB for Client-Side storage

June 29, 2018

IndexedDB

In this post we're going to talk about building an offline website. In this modern era, why would anyone want a website that functions in both an online and offline capacity?? Standard Code has a number of public health clients who operate in parts of the world not as lucky as us. Their access to reliable internet isn't quite the same as ours so they're forced to work with toolsets that are available offline -- that's to say, we spend a lot of time reading word docs and excel spreadsheets with these clients. So we decided to build a proof of concept that demonstrates that once you've "downloaded" a special kind of website, it can operate in an internet-less environment. When internet is available, the data can be uploaded to the cloud. Below we go into detail on how we accomplished that.

This post will hopefully give you the tools you need to setup web-storage using IndexedDB. For my setup I followed a guide on the Mozilla Developer Network's website, so this guide will follow their's closely, but hopefully in a format that is a little easier to understand. However, it should be noted that it would be very helpful to familiarize yourself with IndexedDB, one source to help with that is this article on Mozilla Developer Network's website: Basic Concepts About IndexedDB. To see a working example of the follwoing code check out this Heroku App and the Github repo.

Indexed Database (IndexedDB)

When looking for a solution for web-storage I was pointed to a good article that talks about a variety of different solutions, the article goes through four different types of client-side storage options, their strengths and weaknesses, and shows you how to setup a basic example of each. IndexedDB is an asynchronous API that functions as a collection of "object stores" in which you put your data. These stores function in a similar manor to SQL tables except there are no constraints in the structure.

Step 1: Opening a database

The first step in the whole process begins with a simple request:

var request = indexedDB.open('DatabaseName', 1);

When opening a database there are two parameters that need to be supplied to the open() method. The first of which is the name of the database and the second is the version number. It should be noted that the open() method does not open or start the transaction right away but instead returns an instance of an IndexedDb database with either a success or error value that has to be handled as an event.

const DB_NAME = 'database-name'; const DB_VERSION = 1; const DB_STORE_NAME = 'storeName'; var db; // The above variables are used throughout // the code, place them at the top for // convenience. function openDB() { var request = indexedDB.open(DB_NAME, DB_VERSION); request.onsuccess = function(event) { db = event.target.result; // Do something with request.result }; request.onerror = function(event) { //Do something with request.errorCode }; request.onupgradeneeded = function(event) { db = event.target.result; var store = db.createObjectStore( DB_STORE_NAME, { keyPath: 'keyName', autoIncrement: true } ); }; };

In the code block above you can see how things are handled based on three different results onsuccess, onerror and onupgradeneeded. All of which have their own code that gets executed. One of the things to make note of is that the results of an onsuccessand onupgradeneeded get saved to a variable so it can be called later. If open() gets called and a new database is being setup or the version number has changed then onupgradeneededevent is triggered, after which onsuccesswill be triggered. The onupgradeneeded function contains the logic for creating the object stores, which is where the data gets saved. In the above code the name of the store has been set to keyName and autoIncrement has been set to true, which auto generates a unique number id. This makes it so the data object can be called by that id, allowing the database to function in a similar manor to a SQL database. Also, the onupgradeneeded is the only place where the structure of the actual database is set, this is where you handle all creation and deletion of any object stores.

Adding to the Database

function addToDatabase() { var obj = // data to be saved, store = getObjectStore(DB_STORE_NAME, 'readwrite'), request = store.add(obj); request.onsuccess = function(event) { // Do something if successful }; request.onerror = function(event) { // Do something with error.errorCode }; };

Once a database has been opened or setup the next logical step would be to add something to the database. When it comes to adding data to the actual database the code is pretty straight forward. In the code block above we start with a variable that will contain the data we want to save called obj, followed by store, which contains the object store we want to save the data to and finally the request variable where the add() method is being called. Now one thing to note is that in the store variable calls another method getObjectStore(), this method is used in multiple places so it has been placed in its own function like so:

function getObjectStore(store_name, mode) { var tx = db.transaction(store_name, mode); return tx.objectStore(store_name); }

This function requires two parameters: the store name and mode. Every transaction has three available modes: readonly, readwrite, and versionchange. It should also be noted that versionchange is only used when you want to change the structure of the database. For the purpose of adding data to the database we us the readwrite mode. Once the store variable has been set you can attempt to add the obj to the store, which will result in either onsuccess or onerrorbeing called, from here you decide how to handle things.

Getting from the Database

To make things simpler when retrieving data from the database a cursor can be used to retrieve all values stored in the object store. The code block below shows how to go about doing this.

function retrieveData(store) { if (typeof store == 'undefined') { store = getObjectStore(DB_STORE_NAME, 'readonly'); } var request = store.openCursor(); request.onsuccess = function(event) { var cursor = event.target.result; if (cursor) { // Display/output cursor to see data cursor.continue(); } else { // No more data } }; };

One of the first lines of code in the retrieveData() function is checking to see if the store variable has already been set and passed, if it hasn't then you need to call the getObjectStore function and set it to the store variable. After that everything is pretty straight forward, openCursor() is called on store whose onsuccess method returns all values in the object store one at a time. It should be noted that cursor.keycontains the value of the auto-incremented key and the data can be accessed by cursor.value. Cursors provide much more functionality, but this is a simple example of how to use them.

Delete from Database

The process of deleting data from the database is pretty simple, but requires some nested functions. Something that is important to note is that when deleting data the exact same key used for creation needs to be passed for the deletion, so if the key was a number for creation, then it needs to be a number for deletion. Since an auto-incrmented key was used in this example that key is what is used to delete the data.

function deleteData(key, store) { if (typeof store == 'undefined') { store = getObjectStore(DB_STORE_NAME, 'readwrite'); } var request = store.get(key); request.onsuccess = function(event) { var record = event.target.result; if (typeof record == 'undefined') { console.log("No matching record found"); return; } request = store.delete(key); request.onsuccess = function(event) { // Do something if successful }; request.onerror = function (event) { // Do something with error.errorCode }; }; request.onerror = function (event) { // Do something with error.errorCode }; }

Once again, the first thing to do is check to see that store has either been passed or gets set. After that, take the key that has been passed to the function and call the get()method with the key as the parameter. If that was successful then call the delete() method with the key as its parameter. After the delete() method is called all that is left is to deal with the onsuccess or onerror function.

Conclusion

Following this guide should allow you to be able to setup a simple web app that allows you to create a browser database, add to that database, delete from it and retrieve its content. Its important to note that this kind of storage is very dependent on the browser, its is recommended to use Firefox or Chrome.

References