A Simple TODO list using HTML5 IndexedDB

HTML5 Rocks

Introduction

IndexedDB is new in HTML5. Web Databases are hosted and persisted inside a user's browser. By allowing developers to create applications with rich query abilities it is envisioned that a new breed of web applications will emerge that have the ability to work online and off-line.

The example code in this article demonstrates how to create a very simple todo list manager. It is a very high level tour of some of the features available in HTML5.

This tutorial is a conversion of our A Simple TODO list using HTML5 WebDatabases tutorial and serves to highlight how easy it is to make the transition to IndexedDB.

What is IndexedDB?

IndexedDB is an Object Store. It is not the same as a Relational Database, which has tables, with collections rows and columns. It is an important and fundamental difference and affects the way that you design and build your applications.

In a traditional relational data store we would have a table of "todo" items that store a collection of the users todo data in rows. With columns of named types of data. To insert data, the semantics normally follow: INSERT INTO Todo(id, data, update_time) VALUES (1, "Test", "01/01/2010");

IndexedDB differs in that you create an Object Store for a type of data and simply persist Javascript Objects to that store. Each Object Store can have a collection of Indexes that make it efficient to query and iterate across.

IndexedDB also does away with the notion of a Standard Query Language ( SQL), instead it is replaced with a query against an index which produces a cursor that you use to iterate across the result set.

This tutorial is solely about giving you a real-world example of how to use IndexedDB given the context of existing applications written to use WebSQL.

Why IndexedDB?

On November 18, 2010, the W3C announced that Web SQL database is a deprecated specification. This is a recommendation for web developers to no longer use the technology as effectively the spec will receive no new updates and browser vendors aren't encouraged to support this technology.

The replacement, IndexedDB and the subject of this tutorial is the data-store that developers should use to store and manipulate data on the client-side.

Many major browsers including Chrome, Safari, Opera and nearly all Webkit based mobile devices support WebSQL and will likely maintain support for the foreseeable future.

Pre-requisites

This sample uses a namespace to encapsulate the database logic.

var html5rocks = {};
html5rocks.indexedDB = {};

Asynchronous and Transactional

In the majority of cases where you are using Indexed Database you will be using the Asynchronous API. The Asynchronous API is a non-blocking system and as such will not get data through return values, but rather will get data delivered to a defined callback function.

The IndexedDB support through HTML is transactional. It is not possible to execute commands or open cursors outside of a transaction. There are several types of transactions: read/write transactions, read only and snapshot. In this tutorial, we will be using Read/Write transactions

Step 1. Opening the database

Before we can do anything with the database, it first needs to be opened.

html5rocks.indexedDB.db = null;

html5rocks.indexedDB.open = function() {
  var version = 1;
  var request = indexedDB.open("todos", version);

  request.onsuccess = function(e) {
    html5rocks.indexedDB.db = e.target.result;
    // Do some more stuff in a minute
  };

  request.onerror = html5rocks.indexedDB.onerror;
};

We have opened a database called "todos" and assigned it to our variable db in the html5rocks.indexedDB object. We can now use this to refer to our database through out this tutorial.

Step 2. Creating an Object Store

You can only create object stores inside a "versionchange" transaction. The only way to start a "versionchange" transaction is through the onupgradeneeded callback.

html5rocks.indexedDB.open = function() {
  var version = 1;
  var request = indexedDB.open("todos", version);

  // We can only create Object stores in a versionchange transaction.
  request.onupgradeneeded = function(e) {
    var db = e.target.result;

    // A versionchange transaction is started automatically.
    e.target.transaction.onerror = html5rocks.indexedDB.onerror;

    if(db.objectStoreNames.contains("todo")) {
      db.deleteObjectStore("todo");
    }

    var store = db.createObjectStore("todo",
      {keyPath: "timeStamp"});
  };

  request.onsuccess = function(e) {
    html5rocks.indexedDB.db = e.target.result;
    html5rocks.indexedDB.getAllTodoItems();
  };

  request.onerror = html5rocks.indexedDB.onerror;
};

The above code actually does quite a lot. We define an "open" method in our API, which when called will open the database "todos". The open request isn't executed straight away. Instead an IDBRequest is returned. The indexedDB.open method will be called when the current function exits. This is a little different to how we normally assign asynchronous callbacks, but we get the chance to attach our own listeners to the IDBRequest object before the callbacks are executed.

If the open request is successful and database's version is higher than existing database's version, then our onupgradeneeded callback is executed. In this callback, a "versionchange" transaction will be started automatically.

The onupgradeneeded callback is the only place in our code that we can alter the structure of the database. In it we can create and delete Object Stores and build and remove indexes. If we want to alter the structure of the database again, it is necessary to upgrade database's version.

Object Stores are created with a single call to createObjectStore. The method takes a name of the store, and an parameter object. The parameter object is very important, it lets you define important optional properties. In our case, we define a keyPath that is the property that makes an individual object in the store unique. That property in this example is "timeStamp". "timeStamp" must be present on every object that is stored in the objectStore.

Once the "versionchange" transaction is completed, our onsuccess callback is executed. We call our method getAllTodoItems.

Step 3. Adding data to an object store

We are building a todo list manager so it is pretty important that we are able to add todo items in to the database. This is done as follows:

html5rocks.indexedDB.addTodo = function(todoText) {
  var db = html5rocks.indexedDB.db;
  var trans = db.transaction(["todo"], "readwrite");
  var store = trans.objectStore("todo");
  var request = store.put({
    "text": todoText,
    "timeStamp" : new Date().getTime()
  });

  trans.oncomplete = function(e) {
    // Re-render all the todo's
    html5rocks.indexedDB.getAllTodoItems();
  };

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

The addTodo method is pretty simple, we first get a quick reference to the database object, initiate a "readwrite" transaction and get a reference to our object store.

Now that the application has access to the Object Store, we can issue a simple put command with a basic JSON object. Notice that there is a timeStamp property, that is our unique key for the object and is used as the "keyPath". When the call to put is successful, our onsuccess event is triggered and we are able to render the contents to the screen.

Step 4. Querying the data in a store.

Now that the data is in the database, we need a way to get access to the data in a meaningful way. Luckily, it is pretty straightforward:

html5rocks.indexedDB.getAllTodoItems = function() {
  var todos = document.getElementById("todoItems");
  todos.innerHTML = "";

  var db = html5rocks.indexedDB.db;
  var trans = db.transaction(["todo"], "readwrite");
  var store = trans.objectStore("todo");

  // Get everything in the store;
  var keyRange = IDBKeyRange.lowerBound(0);
  var cursorRequest = store.openCursor(keyRange);

  cursorRequest.onsuccess = function(e) {
    var result = e.target.result;
    if(!!result == false)
      return;

    renderTodo(result.value);
    result.continue();
  };

  cursorRequest.onerror = html5rocks.indexedDB.onerror;
};

Note that all of these commands used in this sample are asynchronous and as such the data is not returned from inside the transaction.

The code makes a transaction and instantiates a keyRange search over the data. The keyRange defines a simple subset of the data we want to query from the store. Given that the keyPath for the store is the numeric timestamp, we set the lowest value of the search to be 0 (anything since the epoch) which just so happens to return all of our data.

There is now a transaction, a reference to the store we want to query and a range that we wish to iterate over. All that remains is to open the cursor and attach an "onsuccess" event.

The results are passed through to the success callback on the cursor, where we render the result. The callback is only fired once per result, so to ensure you keep iterating across the data you need to ensure you call "continue" on your result object.

Step 4a. Rendering data from an Object Store

Once the data has been fetched from the Object Store, the renderTodo method will be called for each result in the cursor.

function renderTodo(row) {
  var todos = document.getElementById("todoItems");
  var li = document.createElement("li");
  var a = document.createElement("a");
  var t = document.createTextNode();
  t.data = row.text;

  a.addEventListener("click", function(e) {
    html5rocks.indexedDB.deleteTodo(row.text);
  });

  a.textContent = " [Delete]";
  li.appendChild(t);
  li.appendChild(a);
  todos.appendChild(li);
}

For a given Todo item we simply take the text and make the UI for the item, including a delete button (so that you can delete a todo item.)

Step 5. Deleting data from a table

html5rocks.indexedDB.deleteTodo = function(id) {
  var db = html5rocks.indexedDB.db;
  var trans = db.transaction(["todo"], "readwrite");
  var store = trans.objectStore("todo");

  var request = store.delete(id);

  trans.oncomplete = function(e) {
    html5rocks.indexedDB.getAllTodoItems();  // Refresh the screen
  };

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

As with the code to put data into an Object Store, deleting data is as simple. Start a transaction, reference the Object Store with your object in and issue a delete command with the unique ID of your object.

Step 6. Hooking it all up

When the page loads, open the database and create the table (if needed) and render any todo items that might already be in the database.

function init() {
  html5rocks.indexedDB.open(); // open displays the data previously saved
}

window.addEventListener("DOMContentLoaded", init, false);

A function that takes the data out of the DOM is needed so, call the html5rocks.indexedDB.addTodo method:

function addTodo() {
  var todo = document.getElementById('todo');

  html5rocks.indexedDB.addTodo(todo.value);
  todo.value = '';
}

The final product

    Comments

    0