Applications need data. For most Web applications, data stores are organized and managed on the server and made available to the client via a network request. As browsers become increasingly more capable, so do the options to store and manipulate application data.

This article introduces you to the in-browser document database known as IndexedDB. With IndexedDB you can create, read, update, and delete large sets of records in much the same way you are accustomed to doing with server-side databases. To experiment with a working version of the code presented in this article, please go to http://craigshoemaker.github.io/indexeddb-intro/, and the full source code is available via the GitHub repository found at https://github.com/craigshoemaker/indexeddb-intro/.

By the end of this tutorial, you'll be familiar with the basic concepts of IndexedDB as well as how to implement a modular JavaScript application that implements a full CRUD application using IndexedDB. Let's begin by looking a little closer at IndexedDB itself.

As browsers become increasingly capable, so do the options to store and manipulate application data.

What is IndexedDB?

In general, there are two different types of databases: relational and document (also known as NoSQL or object). Relational databases like SQL Server, MySQL, and Oracle store sets of data in tables. Document databases like MongoDB, CouchDB, and Redis store sets of data as individual objects. IndexedDB is a document database that exists in a sandboxed context (enforced by respecting the same-origin policy) entirely within the browser. Figure 1 shows data in IndexedDB that highlights the structure of the database.

IndexedDB is a document database that exists in a sandboxed context entirely within the browser.

Figure 1: Developer tools inspecting an object store
Figure 1: Developer tools inspecting an object store

For full documentation on the entirety of the IndexedDB API, please refer to https://developer.mozilla.org/docs/Web/API/IndexedDB_API.

Design Paradigms

The architecture of IndexedDB resembles similar types of design paradigms as found in some of the popular server-side NoSQL database implementations. Object-oriented data is persisted in what are called object stores and all actions are request-based and executed within a transaction scope. The event lifecycle gives you the ability to control the configuration of the database and errors are managed throughout the API via error bubbling.

Object Stores

The foundation of an IndexedDB database is the object store. If you are experienced in relational databases, you can generally equate an object store to a database table. Object stores include one or more indices that operate as a key/pair value in the store and provide a way to quickly locate data.

As you configure an object store, you must select a key type of the store. Keys can exist within a store as “in-line” or “out-of-line” keys. In-line keys enforce uniqueness in the object store by referencing a path in the data object. To illustrate, consider a Person object that includes an Email Address property. You could configure your store to use an in-line key of emailAddress that enforces uniqueness across the store via data in the persisted object. Alternatively, out-of-line keys identify uniqueness through values that are independent to the data. In this case, you can liken the out-of-line key to an integer value in a relational database that acts as the primary key for the record.

Figure 1 shows task data persisted in the tasks object store that employs an in-line key. The key in this case corresponds to the ID value of the object.

Transaction-Based

Unlike some traditional relational database implementations, every operation against the database is executed in the context of a transaction. Transaction scopes affect one or more object stores at a time and you define this by passing in an array of object store names in to the function that creates a transaction scope.

The secondary argument involved in creating a transaction is the transaction mode. When requesting a transaction, you must decide whether to request access in a read-only or read-write mode. Transactions are resource intensive, so if you have no need change data in a data store, you only need to request access to the collection of object stores in a read-only mode.

Listing 2 demonstrates how to create a transaction using the appropriate mode, and is discussed in detail in the Implementing Database-Specific Code section of this article.

Request-Based

There's a recurring theme that you may have noticed up until this point. Each operation against the database is described as involving a request to open the database, access an object store, and so on. The IndexedDB API is inherently request-based, which is an indication of the asynchronous nature of the API. For each operation you execute against the database, you must first create a request for that operation. As the request is being fulfilled, you can respond to events and errors that are produced as a result of a request.

The code implemented in this article demonstrates how requests are used to open the database, create a transaction, read object store contents, write to an object store, and empty an object store.

Request Lifecycle of an Open Database

IndexedDB uses an even lifecycle to manage the open and configuration operations of the database. Figure 2 demonstrates how an open request raises the upgrade needed event under certain circumstances.

Figure 2      : The IndexedDB open request life cycle
Figure 2 : The IndexedDB open request life cycle

All interaction with the database begins with an open request. When an attempt to open the database is made, you must pass in an integer value that represents the requested version number of the database. During the open request, the browser checks the version number that you pass into the open request against the actual version number of the database. If the requested version number is higher than the current version in the browser (or if there is no existing database at the time), the upgradeneeded event fires. During the upgradeneeded event, you have an opportunity to manipulate object stores by adding or removing stores, keys, and indices.

If the requested version of the database is equal to the current version in the browser, or when the upgrade procedure is complete, an open database is returned to the caller.

Error Bubbling

Of course, from time to time, a request may not complete as expected. The IndexedDB API features error bubbling to help keep track and manage errors. If a specific request encounters an error, you can attempt to handle the error on the request object, or you can allow the error to “bubble up” through the call stack. This bubbling nature makes it so you are not required to implement specific error handling operations for each and every request, but may choose to only add error handling at a higher level, which gives you an opportunity to keep your error-handling code concise. The example implemented in this article handles errors at a high level so that any errors from more fine-grained operations bubble up to the generalized error handling logic.

Browser Support

Perhaps the most important question when developing applications for the Web is: “Will the browsers support what I'm trying to do?” Although browser support for IndexedDB continues to grow, the adoption rate is not as ubiquitous today as we might hope. Figure 3 shows how the website https://caniuse.com/ reports that global support for IndexedDB is a bit over 66%. The latest versions of Firefox, Chrome, Opera, Safar, iOS Safari, and Android all fully support IndexedDB, and Internet Explorer and Blackberry feature partial support. Although this list of supporters is encouraging, it doesn't tell the whole story.

Figure       3      : Browser support from caniuse.com for IndexedDB
Figure 3 : Browser support from caniuse.com for IndexedDB

Only the very latest versions of Safari and iOS Safari support IndexedDB. Again, according to https://caniuse.com/, this only accounts for approximately 0.01% of global browser usage. IndexedDB is not a modern Web API that you can take support for granted, but you will be able to soon.

Another Option

Browser support for local databases didn't begin with the IndexedDB implementation, but rather it's a newer approach that has come after the WebSQL implementation. Similar to IndexedDB, WebSQL is a client-side database, but it's implemented as a relational database that uses structured query language (SQL) to communicate with the database. The history surrounding WebSQL is full of twists and turns, but the bottom line is that none of the major browser vendors are continuing support for WebSQL.

If WebSQL is effectively an abandoned technology, why even bring it up? Interestingly, WebSQL enjoys solid support among browsers. Chrome, Safari, iOS Safari, and Android browsers all support WebSQL. Plus, not only do the latest versions of these browsers offer support, but support is available many versions behind the latest and greatest of these browsers. What's interesting is that if you add the support for WebSQL to support for IndexedDB, you find that all of a sudden, a great number of browser vendors and versions support some incarnation of an in-browser database.

So if your application is truly in need of a client-side database and you want to achieve the highest levels of adoption possible, perhaps your application may look to support a client-side data architecture that falls back to WebSQL if IndexedDB is not available. Although there's a stark difference in how a document database and a relational database manage data, you can build an application that uses local databases as long as your have the right abstractions in place.

Is IndexedDB Right for My Application?

Now for the million dollar question: “Is IndexedDB right for my application?” As always, the answer is most certainly: “It depends.” The first place you might look when attempting to persist data on the client is HTML5 local storage. Local storage enjoys widespread browser adoption and features a ridiculously easy-to-use API. The simplicity has its advantages, but liabilities are found in its inability to support complex search strategies, store large sets of data, and provide transactional support.

IndexedDB is a database. So when you're trying to make a decision about the client, consider how you might select a database as a persistence medium on the server. Questions you may ask yourself to help determine if a client-side database is right for your application include:

  • Do your users access your application with browsers that support the IndexedDB API?
  • Do you need to store a significant amount of data on the client?
  • Do you need to quickly locate individual data points within a large set of data?
  • Does your architecture require transactional support on the client?

If you answer “yes” to any of these questions, there is a good chance that IndexedDB is a good candidate for your application.

Using IndexedDB

Now that you've had an opportunity to become familiar with some of the overall concepts, the next step is to begin implementing an application based on IndexedDB. One of the first steps required is to normalize the different browser implementations of IndexedDB. You can easily do this by adding some checks for the various vendor-specific options and set them equal to the official object names off the window object. The following listing demonstrates how the final result of window.indexedDB, window.IDBTransaction and window.IDBKeyRange are all updated to be set to the appropriate browser-specific implementations.

window.indexedDB = window.indexedDB ||
                   window.mozIndexedDB ||
                   window.webkitIndexedDB ||
                   window.msIndexedDB;

window.IDBTransaction = window.IDBTransaction ||
                        window.webkitIDBTransaction ||
                        window.msIDBTransaction;

window.IDBKeyRange = window.IDBKeyRange ||
                     window.webkitIDBKeyRange ||
                     window.msIDBKeyRange;

Now that each of the database-related global objects hold the correct version, the application is ready to begin working with IndexedDB.

Application Overview

In this tutorial, you learn to create a modular JavaScript application that persists data to IndexedDB. To get a sense of how the application works, refer to Figure 4, which depicts the Tasks application in its blank state. From here you can add new tasks to the list. Figure 5 shows the screen with a few tasks entered into the system. Figure 6 indicates how to delete a task, and Figure 7 illustrates the application while editing a task.

Figure 4: The blank task application
Figure 4: The blank task application
Figure 5: Task list
Figure 5: Task list
Figure 6: Deleting a task
Figure 6: Deleting a task
Figure 7: Editing a task
Figure 7: Editing a task

Now that you're familiar with the functionality of the application, the next step is to begin laying the foundation for the website.

Laying the Foundation

This example begins by implementing a module that's responsible for reading data from the database, inserting new objects, updating existing object, deleting individual objects, and exposing the option to delete all objects in an object store. The code implemented in this example is generic data-access code that you could use on any object store.

The module is implemented as an immediately invoked function expression (IIFE), which uses an object literal to provide structure. The following code is an excerpt from the module that illustrates its basic structure.

(function (window) {

    'use strict';

    var db = {
        /* implementation here */
    };

    window.app = window.app || {};
    window.app.db = db;

    
}(window));

Using a structure like this keeps all the logic for this application wrapped up in a single object named app. Further, the database-specific code is in a child object of app named db.

This code for this module uses an IIFE and passes in the window object to ensure proper scoping in the module. The declaration of use strict ensures that the code in this function is evaluated under the strictest compiler rules. The db object is used as the main container for all of the functions that interact with the database. Lastly, the window object is checked to see if an instance of app exists; if it's present, this module uses the current instance and if not, a new object is created. Once the app object is successfully returned or created, the db object is appended to the app object.

The rest of this article adds code into the nested db object (in place of the implementation here comment) in order to provide database-specific logic for the application. Therefore, as you see functions defined in later sections of this article, know that the parent db object is removed for brevity, but all other functions are members of the db object. For a full listing of the database module see Listing 2.

Listing 2: Full source for database-specific code (index.db.js)


// index.db.js

;

window.indexedDB = window.indexedDB ||
                   window.mozIndexedDB ||
                   window.webkitIndexedDB ||
                   window.msIndexedDB;

window.IDBTransaction = window.IDBTransaction ||
                        window.webkitIDBTransaction ||
                        window.msIDBTransaction;

window.IDBKeyRange = window.IDBKeyRange ||
                     window.webkitIDBKeyRange ||
                     window.msIDBKeyRange;

(function(window){

    'use strict';

    var db = {

        version: 1, // important: only use whole numbers!

        objectStoreName: 'tasks',

        instance: {},

        upgrade: function (e) {

            var
                _db = e.target.result,
                names = _db.objectStoreNames,
                name = db.objectStoreName;

            if (!names.contains(name)) {

                _db.createObjectStore(
                name,
                {
                    keyPath: 'id',
                    autoIncrement: true
                });
            }
    },

    errorHandler: function (error) {
        window.alert('error: ' + error.target.code);
        debugger;
    },

    open: function (callback) {

        var request = window.indexedDB.open(
            db.objectStoreName, db.version);

        request.onerror = db.errorHandler;

        request.onupgradeneeded = db.upgrade;

        request.onsuccess = function (e) {

            db.instance = request.result;

            db.instance.onerror = db.errorHandler;

            callback();
        };
    },

    getObjectStore: function (mode) {

        var txn, store;

        mode = mode || 'readonly';

        txn = db.instance.transaction(
            [db.objectStoreName], mode);

        store = txn.objectStore(
            db.objectStoreName);

        return store;
    },

    save: function (data, callback) {

        db.open(function () {

            var store, request,
                mode = 'readwrite';

            store = db.getObjectStore(mode),

            request = data.id ?
                store.put(data) :
                store.add(data);

            request.onsuccess = callback;
        });
    },

    getAll: function (callback) {

        db.open(function () {

            var
                store = db.getObjectStore(),
                cursor = store.openCursor(),
                data = [];

            cursor.onsuccess = function (e) {

                var result = e.target.result;

                    if (result &&
                        result !== null) {

                        data.push(result.value);
                        result.continue();

                    } else {

                    callback(data);
                    }
            };

        });
    },

    get: function (id, callback) {

        id = parseInt(id);

        db.open(function () {

            var
                store = db.getObjectStore(),
                request = store.get(id);

            request.onsuccess = function (e){
                callback(e.target.result);
            };
        });
    },

    'delete': function (id, callback) {

        id = parseInt(id);

        db.open(function () {

            var
                mode = 'readwrite',
                store, request;

            store = db.getObjectStore(mode);

            request = store.delete(id);

            request.onsuccess = callback;
        });
    },

    deleteAll: function (callback) {

        db.open(function () {

            var mode, store, request;

            mode = 'readwrite';
            store = db.getObjectStore(mode);
            request = store.clear();

            request.onsuccess = callback;
        });

    }
    };

    window.app = window.app || {};
    window.app.db = db;

}(window));

Implementing Database-Specific Code

Each operation against the database is associated with the prerequisite to have an open database. As the database is being opened, the database version is inspected to see if any changes are required to the database. The following code shows how the module keeps track of the current version, the object store name, and a member to hold the current instance of the database once the open request is complete.

version: 1,

objectStoreName: 'tasks',

instance: {},

Here, as the database open request is issued, the module asks for version 1 of the database. If the database doesn't exist, or the version is less than version 1, the upgradeneeded event fires before the open request completes. This module is set up to only work with a single object store, so the name is defined here. Finally, the instance member is created to hold the current instance of the open database once the request is completed.

The next operation to implement is the event handler for the upgradeneeded event. Here, the current object store names are inspected to see if the requested object store name is present; if it doesn't exist, the object store is created.

upgrade: function (e) {
    var
        _db = e.target.result,
        names = _db.objectStoreNames,
        name = db.objectStoreName;

    if (!names.contains(name)) {
        _db.createObjectStore(
            name,
                {
                    keyPath: 'id',
                    autoIncrement: true
                });
    }
},

The database is accessed inside this event handler through the event argument via e.target.result. The current list of object store names is available through an array of strings found at _db.objectStoreName. Now, if the object store doesn't exist, it's created by passing in the object store name and defining the store's key as autoIncrement associated to the data's ID member.

The next function in the module is used to catch errors as they bubble up through different requests in created in the module.

errorHandler: function (error) {
    window.alert('error: ' + error.target.code);
    debugger;
},

Here, errorHandler displays any errors in an alert box. This function is kept intentionally simple and development-friendly so that as you learn to use IndexedDB, you can easily see any errors as they occur. When you're ready to use this module in production, you need to implement some sort of error-handling code in this function that works within the context of your application.

Now that the generalities are implemented, the rest of this section demonstrates how to implement specific actions performed against the database. The first function to examine is the open function.

open: function (callback) {
    var request = window.indexedDB.open(
        db.objectStoreName, db.version);

    request.onerror = db.errorHandler;
    request.onupgradeneeded = db.upgrade;

    request.onsuccess = function (e) {
        db.instance = request.result;
        db.instance.onerror = db.errorHandler;

        callback();
    };
},

The open function attempts to open the database and then executes the callback function to signal that the database is successfully open and ready for use. The open request is created by accessing window.indexedDB and calling the open function. This function accepts the object store name you wish to open and the version of the database you wish to use.

Once an instance of the request is available, the first order of business is to set the error handler and the upgrade function. Remember, as the database is being opened, if the script is requesting a higher version of the database than what is present in the browser (or if the database doesn't exist), the upgrade function is run. If, however, the requested database version matches the current database version and there are no errors, the success event is fired.

If everything succeeds, the open instance of the database is available from the result property of the request instance that's cached into the module's instance property. Then, the onerror event is set to the module's errorHandler to act as a catch-all error handler for any further requests. Finally, the callback is executed to signal to the caller that the database is open and properly configured for use.

The next function to implement is a helper function that returns the requested object store.

getObjectStore: function (mode) {

    var txn, store;

    mode = mode || 'readonly';

    txn = db.instance.transaction([db.objectStoreName], mode);

    store = txn.objectStore(db.objectStoreName);

    return store;
},

Here, getObjectStore accepts a mode parameter that allows you to control whether or not the store is requested as read-only or read-write mode. For this function, the default mode is readonly.

Each operation against an object store is executed in the context of a transaction. The transaction request accepts an array of object store names. This function is configured to only work with one object store at a time, but should you need to work against more than one store in a transaction, you pass in more object store names in the array. The second parameter of the transaction function is the mode.

Once the transaction request is available, you can call the objectStore function to gain access of the instance of the object store by passing in the desired object store name. The rest of the functions in this module use getObjectStore to gain access to the object store.

The next function to implement is the save function, which either performs an insert or update operation, depending on whether the data passed in has an ID value or not.

save: function (data, callback) {

    db.open(function () {

        var store, request,

            mode = 'readwrite';

        store = db.getObjectStore(mode),
        request = data.id ?

            store.put(data) :
            store.add(data);

        request.onsuccess = callback;
    });
},

The two parameters used in the save function are instances of the data object being saved and the callback that executes when the operation is a success. The readwrite mode is used for writing data to the database and is passed in to getObjectStore in order to get a writeable instance of the object store. Then, the data object is inspected to see if the ID member exists. If an ID value is present, the data must be updated and the put function is called, which creates the persistence request. Otherwise, if the ID is not present, this is new data and an add request is returned. Finally, whether or not a put or add request is executed, the success event handler is set to the callback function that tells the calling script that everything went as planned.

Code for the next section is found in Listing 1. The getAll function begins by opening the database and accessing the object store, which sets values for store and cursor respectively. The cursor variable is set to a database cursor that allows iteration over the data in the object store. The data variable is set to a blank array that acts as the container for the data, which is returned up to the calling code.

Listing 1: Implementing the getAll function


getAll: function (callback) {

    db.open(function () {

        var
            store = db.getObjectStore(),
            cursor = store.openCursor(),
            data = [];

        cursor.onsuccess = function (e) {

            var result = e.target.result;

            if (result &&
                result !== null) {

                data.push(result.value);
                result.continue();

            } else {

                callback(data);
            }
        };

    });
},

As data is accessed in the store, the cursor iterates through each record in the database that fires the onsuccess event handler. As each record is accessed, the data from the store is available through the event arguments via e.target.result. Although the actual data is found in the value property from the target's result, you first need to make sure that there is a valid value for result before attempting to access the value property. If result exists, you can add the result's value into the data array and then call the continue function on the result object to continue iteration through the object store. Finally, if there is no result, iteration through the store's data is complete and the callback is executed by passing the data into the callback.

Now that the module is able to get all data from the data store, the next function to implement is responsible for accessing a single record.

get: function (id, callback) {

    id = parseInt(id);

    db.open(function () {

        var
            store = db.getObjectStore(),
            request = store.get(id);
            request.onsuccess = function (e){
                callback(e.target.result);
            };
    });
},

The first operation that the get function performs is to convert the value of the id parameter into an integer. Depending on how the function is called, a string or integer may be passed to the function. This implementation skips dealing with the condition of what to do if the given string can't convert into an integer. Once an id value is prepared, the database is opened and the object store is accessed. The meat of the function is found by getting access to a get request. When the request is successful, the callback is executed by passing in the value of e.target.result, which is the single record as requested by calling the get function.

Now that the save and selection operations are present, the module also needs to be able to remove data from an object store.

'delete': function (id, callback) {
    id = parseInt(id);
    db.open(function () {

    var
        mode = 'readwrite',
        store, request;

    store = db.getObjectStore(mode);

    request = store.delete(id);

    request.onsuccess = callback;
    });
},

The name of the delete function is wrapped in single quotes because delete is a reserved word in JavaScript. This is a judgment call on your part. You could choose to name the function del or some other name, but delete is used in this module to keep the API as expressive as possible.

The parameters passed into the delete function are the object's id and a callback function. In order to keep this implementation simple, the delete function's contract is to work with integer values for ids. You may choose to create a more robust implementation to employ a fail case callback should the id value not parse into an integer, but for instruction's sake the code sample is left intentionally rudimentary.

Once the id value is ensured converted into an integer, the database is opened, an instance of a writeable object store is acquired and the delete function is called passing in the id value. When the request is a success, the callback function is executed.

In some cases, you may want to delete all of the records in an object store. In this case, you access the store and clear all contents.

deleteAll: function (callback) {

    db.open(function () {

    var mode, store, request;

    mode = 'readwrite';
    store = db.getObjectStore(mode);
    request = store.clear();

    request.onsuccess = callback;
    });
}

Here the deleteAll function is responsible for opening the database and accessing a writeable instance of the object store. Once the store is available, a new request is created by calling the clear function. Once the clear operation is a success, the callback function is executed.

Implementing User Interface-Specific Code

Now that all the database-specific code is wrapped up in the app.db module, the user interface-specific code can use this module to interface with the database. The full listing for the user interface-specific code (index.ui.js) is available in Listing 3 and the full HTML source for the page (index.html) is available in Listing 4.

Listing 3: Full source for user interface-specific code (index.ui.js)


// index.ui.js

;

(function ($, Modernizr, app) {

    'use strict';

    $(function(){

        if(!Modernizr.indexeddb){
            $('#unsupported-message').show();
            $('#ui-container').hide();
            return;
        }

        var
            $deleteAllBtn = $('#delete-all-btn'),
            $titleText = $('#title-text'),
            $notesText = $('#notes-text'),
            $idHidden = $('#id-hidden'),
            $clearButton = $('#clear-button'),
            $saveButton = $('#save-button'),
            $listContainer = $('#list-container'),
            $noteTemplate = $('#note-template'),
            $emptyNote = $('#empty-note');

        var addNoTasksMessage = function(){
            $listContainer.append(
                $emptyNote.html());
        };

        var bindData = function (data) {

            $listContainer.html('');

            if(data.length === 0){
                addNoTasksMessage();
                return;
        }

        data.forEach(function (note) {
            var m = $noteTemplate.html();
            m = m.replace(/{ID}/g, note.id);
            m = m.replace(/{TITLE}/g, note.title);
            $listContainer.append(m);
        });
    };

    var clearUI = function(){
        $titleText.val('').focus();
        $notesText.val('');
        $idHidden.val('');
    };

    // select individual item
    $listContainer.on('click', 'a[data-id]',

    function (e) {

        var id, current;

        e.preventDefault();

        current = e.currentTarget;
        id = $(current).attr('data-id');

        app.db.get(id, function (note) {
            $titleText.val(note.title);
            $notesText.val(note.text);
            $idHidden.val(note.id);
        });

        return false;
    });

    // delete item
    $listContainer.on('click', 'i[data-id]',

    function (e) {

        var id, current;

        e.preventDefault();

        current = e.currentTarget;
        id = $(current).attr('data-id');

        app.db.delete(id, function(){
            app.db.getAll(bindData);
            clearUI();
        });

        return false;
    });

    $clearButton.click(function(e){
        e.preventDefault();
        clearUI();
        return false;
    });

    $saveButton.click(function (e) {

        var title = $titleText.val();

        if (title.length === 0) {
            return;
        }

        var note = {
            title: title,
            text: $notesText.val()
        };

        var id = $idHidden.val();

        if(id !== ''){
            note.id = parseInt(id);
        }

        app.db.save(note, function(){
            app.db.getAll(bindData);
            clearUI();
        });
        
    });

    $deleteAllBtn.click(function (e) {

        e.preventDefault();

        app.db.deleteAll(function () {
            $listContainer.html('');
            addNoTasksMessage();
            clearUI();
        });

        return false;
    });

    app.db.errorHandler = function (e) {
        window.alert('error: ' + e.target.code);
        debugger;
    };

    app.db.getAll(bindData);

    });

}(jQuery, Modernizr, window.app));

Listing 4: Full HTML source (index.html)


<!doctype html>
<html lang="en-US">
    <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Introduction to IndexedDB</title>
    <meta name="description"
          content="Introduction to IndexedDB">
    <meta name="viewport"
          content="width=device-width, initial-scale=1">
    <link rel="stylesheet"
          href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css";>
    <link rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs
                /font-awesome/4.1.0/css/font-awesome.min.css">
    <link rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs
                /font-awesome/4.1.0/fonts/FontAwesome.otf">
    <link rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs
                /font-awesome/4.1.0/fonts/fontawesome-webfont.eot">
    <link rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs
                /font-awesome/4.1.0/fonts/fontawesome-webfont.svg">
    <link rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs
                /font-awesome/4.1.0/fonts/fontawesome-webfont.ttf">
    <link rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs
                /font-awesome/4.1.0/fonts/fontawesome-webfont.woff">
    <style>
        h1 {
               text-align: center;
               color:#999;
        }

        ul li {
            font-size: 1.35em;
            margin-top: 1em;
            margin-bottom: 1em;
        }

        ul li.small {
            font-style: italic;
        }

        footer {
            margin-top: 25px;
            border-top: 1px solid #eee;
            padding-top: 25px;
        }

        i[data-id] {
            cursor: pointer;
            color: #eee;
        }

        i[data-id]:hover {
            color: #c75a6d;
        }

        .push-down {
            margin-top: 25px;
        }

        #save-button {
            margin-left: 10px;
        }
    </style>
    <script src="//cdnjs.cloudflare.com/ajax/libs/modernizr
                 /2.8.2/modernizr.min.js"></script>
    </head>
    <body class="container">
        <h1>Tasks</h1>
        <div id="unsupported-message"
             class="alert alert-warning"
             style="display:none;">
             <b>Aww snap!</b> Your browser does not support indexedDB.
        </div>
        <div id="ui-container" class="row">
        <div class="col-sm-3">

            <a href="#" id="delete-all-btn" class="btn-xs">
                <i class="fa fa-trash-o"></i> Delete All</a>

            <hr/>

            <ul id="list-container" class="list-unstyled"></ul>

        </div>
        <div class="col-sm-8 push-down">

            <input type="hidden" id="id-hidden" />

            <input
                   id="title-text"
                   type="text"
                   class="form-control"
                   tabindex="1"
                   placeholder="title"
                   autofocus /><br />

            <textarea
                   id="notes-text"
                   class="form-control"
                   tabindex="2"
                   placeholder="text"></textarea>

            <div class="pull-right push-down">

                <a href="#" id="clear-button" tabindex="4">Clear</a>

                <button id="save-button"
                        tabindex="3"
                        class="btn btn-default btn-primary">
                            <i class="fa fa-save"></i> Save</button>
            </div>
        </div>
    </div>
    <footer class="small text-muted text-center">by
        <a href="http://craigshoemaker.net" target="_blank">Craig Shoemaker</a>
        <a href="http://twitter.com/craigshoemaker" target="_blank">
            <i class="fa fa-twitter"></i></a>
    </footer>
    <script id="note-template" type="text/template">
        <li>
            <i data-id="{ID}" class="fa fa-minus-circle"></i>
            <a href="#" data-id="{ID}">{TITLE}</a>
        </li>
    </script>
    <script id="empty-note" type="text/template">
        <li class="text-muted small">No tasks</li>
    </script>
    <script src="//ajax.googleapis.com/ajax/libs
                 /jquery/1.11.1/jquery.min.js"></script>
    <script src="index.db.js" type="text/javascript"></script>
    <script src="index.ui.js" type="text/javascript"></script>
    </body>
</html>

Conclusion

As the needs of your applications grow, you may find advantages to being able to efficiently store significant amounts of data on the client. IndexedDB is a document database implementation available directly in the browser that features asynchronous transactional support. Although the browser support may not be yet taken for granted, in the right situations, Web applications that integrate with IndexedDB can feature robust client-side data access capabilities.