Adding IndexedDB Functionality to an Application
Join the DZone community and get the full member experience.
Join For FreeThis article, based on chapter 5 of HTML5 in Action by Robert Crowther, Joe Lennon, and Ash Blue, shows you how you use the IndexedDB API to create, connect to, and use a database that is stored on the client-side in the user’s Web browser.
The IndexedDB API can be notoriously complex at first glance, particularly if you do not have experience with writing JavaScript code that works asynchronously. The best way to learn how to use it is through an example, and in this article you will do just that, adding database functionality to a sample My Tasks application.
IndexedDB was added to HTML5 quite late in the specification
process. As a result, browser support for it has been much slower than with
other parts of the specification. Before IndexedDB, HTML5 included a
client-side database specification known as Web SQL, which defined an API for a
full relational database that would live in the browser. Although Web SQL is
now dead and is no longer part of HTML5, many browser vendors had already
provided decent support for it, particularly mobile browsers. As a result, we
will use Web SQL as a fallback in our sample app, ensuring that it runs on Web
SQL compatible browsers, even if they do not yet support IndexedDB. As
IndexedDB support becomes more widespread, our app will automatically choose to
use it over Web SQL where available.
In this article, you will use the IndexedDB API to create, connect to, and use a database that is stored on the client side in the user’s Web browser. You will implement the Add Task form view in the application, storing new tasks in the database. Next, you will update existing tasks, allowing users to mark tasks complete by checking the relevant checkbox. You will also enable users to delete tasks, either one at a time by clicking on a single task’s Delete button or all at once using the Clear Tasks feature. Finally, you will load existing tasks from IndexedDB and display them to the user, complete with the UI features required to allow them to update and delete the task if required.
Connecting to the database
In order to add IndexedDB and Web SQL functionality to the My Tasks app, you will need to add the code from the forthcoming listings to the app.js external JavaScript file. The majority of the functions you add will be added to a new tasksDB object, which will live alongside the main app object. Let’s start off by doing some name mapping for prefixed implementations in Firefox and Chrome and defining our basic tasksDB object shell.
Listing 1 The shell for our data-related functions
if('mozIndexedDB' in window || 'webkitIndexedDB' in window) { window.indexedDB = window.mozIndexedDB || window.webkitIndexedDB; window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction; window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange; } var tasksDB = { db: null };
Let’s jump right in and start adding functions to the tasksDB object. The first function you need to define is open, which will be responsible for opening a connection to either an IndexedDB or Web SQL database, creating an object store or table if required, and firing the load function to retrieve any existing tasks. Add the code from the following listing inside the tasksDB object.
Listing 2 The open function
open: function() { if('indexedDB' in window) { var req = window.indexedDB.open("tasks"); #1 req.onsuccess = function(event) { tasksDB.db = event.target.result; #2 if(tasksDB.db.version != "1") { req = tasksDB.db.setVersion("1"); req.onsuccess = function(e) { var objStore = tasksDB.db.createObjectStore( #3 "tasks", { keyPath: "id" }); #3 tasksDB.load(); } } else { tasksDB.load(); } } } else if('openDatabase' in window) { tasksDB.db = openDatabase('tasks', '1.0', 'Tasks database', (5*1024*1024)); tasksDB.db.transaction(function(tx) { #4 tx.executeSql('CREATE TABLE IF NOT EXISTS tasks (' + 'id INTEGER PRIMARY KEY ASC, desc TEXT, due DATETIME,' + 'complete BOOLEAN)', [], tasksDB.onsuccess, tasksDB.onerror); }); tasksDB.load(); } }
#1 Opens database
#2 Connects to database
#3 Creates object store
#4 Starts SQL transaction
This might seem like a lot of code to open a connection to a database, but it also takes care of creating an object store if necessary and provides a Web SQL fallback for browsers that don’t support IndexedDB. The function checks if the user agent support can use IndexedDB; if it can, it will immediately attempt to open a connection to a database named “tasks” (#1). As IndexedDB APIs are asynchronous, define a callback function that allows you to get a handle to the database object (#2), which you assign to the tasksDB.db property. You can use this property in other functions for convenience. After checking the version number of the database, call the setVersion function if you need to create an object store, which must be done in the scope of a setVersion transaction. You then actually create the object store itself (#3), giving it the name “tasks” and setting the key property as “id”.
As previously mentioned, in case IndexedDB is not supported (as it is not in current versions of many desktop and mobile Web browsers), you can provide a Web SQL fallback, which will allow the application to work in those browsers. Keep in mind that there are many older browsers (and even not-so-old versions of Internet Explorer) that won’t support any of these new HTML5 features, so be sure to use a relatively recent browser. The openDatabase API allows you to get a connection to a Web SQL database. In this case, you give it the name tasks, a version of 1.0, a description, and a maximum size of 5 megabytes. Then, use a transaction to issue an executeSql command (#4), which will create a table named tasks if it does not already exist.
Adding new tasks to the database
Next, let’s add the code which will allow users to add a new task. Again in the tasksDB object, add two new functions, addTask and addSuccess, as outlined in the following listing.
Listing 3 Adding tasks
addTask: function() { var task = { id: new Date().getTime(), desc: document.getElementById("txt_desc").value, due: document.getElementById("txt_due").value, complete: false } if('indexedDB' in window) { var tx = tasksDB.db.transaction(["tasks"], #1 IDBTransaction.READ_WRITE, 0); var objStore = tx.objectStore("tasks"); var req = objStore.add(task); #2 req.onsuccess = tasksDB.addSuccess; req.onerror = tasksDB.onerror; } else if('openDatabase' in window) { tasksDB.db.transaction(function(tx) { tx.executeSql('INSERT INTO tasks(desc, due, complete) VALUES(' '?, ?, ?)', [task.desc, task.due, task.complete], tasksDB.addSuccess, tasksDB.onerror); }); } return false; }, addSuccess: function() { tasksDB.load(); #3 alert("Your task was successfully added", "Task added"); document.getElementById("txt_desc").value = ""; document.getElementById("txt_due").value = ""; location.hash = "#list"; #4 }
#1 Initiates transaction
#2 Adds task
#3 Reloads tasks
#4 Redirects to Task List
In the addTask function, you first build up a task object, and then initiate a database transaction (#1). Then, you call the add API on the object store to save the task object (#2), defining a callback function tasksDB.addSuccess, which will be fired when the object has been successfully added to the object store. Again, perform a similar fallback Web SQL operation if IndexedDB is not available. The addSuccess function reloads the tasks from the database (#3) and displays a message, resetting form fields, and then redirects the user to the main task list view (#4) so they can see their new task amongst their other tasks. The final piece of the puzzle with regards to the Add Task feature is to connect the new tasksDB.addTask function up to the relevant form. You also need to open the connection to the database at application launch. In the app.launch function, add the following code to the end of the function:
document.forms.add.onsubmit = tasksDB.addTask; tasksDB.open();
Modifying existing tasks
Updating, deleting, and clearing tasks works in a similar manner. You will create functions updateTask and deleteTask, with a drop and dropSuccess callback combination used for the clearing tasks functionality. The code for all of these functions is as follows.
Listing 4 Updating, deleting and clearing tasks
updateTask: function(task) { if('indexedDB' in window) { var tx = tasksDB.db.transaction(["tasks"], IDBTransaction.READ_WRITE, 0); var objStore = tx.objectStore("tasks"); var req = objStore.put(task); #1 req.onerror = tasksDB.onerror; } else if('openDatabase' in window) { var complete = 0; if(task.complete) complete = 1; tasksDB.db.transaction(function(tx) { tx.executeSql('UPDATE tasks SET complete = ? WHERE id = ?', [complete, task.id], tasksDB.load, tasksDB.onerror); }); } }, deleteTask: function(id) { if('indexedDB' in window) { var tx = tasksDB.db.transaction(["tasks"], IDBTransaction.READ_WRITE, 0); var objStore = tx.objectStore("tasks"); var req = objStore['delete'](id); #2 req.onsuccess = function(event) { tasksDB.load(); } req.onerror = tasksDB.onerror; } else if('openDatabase' in window) { tasksDB.db.transaction(function(tx) { tx.executeSql('DELETE FROM tasks WHERE id = ?', [id], tasksDB.load, tasksDB.onerror); }); } }, drop: function() { if('indexedDB' in window) { var tx = tasksDB.db.transaction(["tasks"], IDBTransaction.READ_WRITE, 0); var objStore = tx.objectStore("tasks"); var req = objStore.clear(); #3 req.onsuccess = tasksDB.dropSuccess, req.onerror = tasksDB.onerror; } else if('openDatabase' in window) { tasksDB.db.transaction(function(tx) { tx.executeSql('DELETE FROM tasks', [], tasksDB.dropSuccess, tasksDB.onerror); }); } }, dropSuccess: function() { tasksDB.load(); app.loadSettings(); alert("Settings and tasks reset successfully", "Reset complete"); location.hash = "#list"; }
#1 Updates task
#2 Deletes task
#3 Clears object store
Much of the code in these functions is similar to the addTask function—you typically need to create a transaction object, get a reference to the object store, and then call the relevant API on that reference. In the updateTask function, you use the put API (#1) with the task object as an argument to update the task in the database. It is imperative that the task object has the correct key value or it may create a new object in the store rather than update the existing one. For deleting tasks, use the delete API function. Some browsers don’t like this as delete is a reserved word in JavaScript, so, to be safe, you should use the square bracket notation to call the API (#2). Clearing the object store is as simple as calling the clear API (#3). In this case, attach a callback function to the success event on this API, which will reload the tasks from the database (should now be empty), reload settings from localStorage and redirect the user to the task list view.
Back in the app object, you should add the following line to the resetData function, just below the line that clears the localStorage settings data:
tasksDB.drop();
Loading tasks from the database
The final piece of functionality that you need to add to your application is actually loading tasks themselves into the Task List view, creating event listeners that allow the user to update the task by checking the box to indicate it is completed, and deleting the task by tapping the red button that appears on the right side of each task. This code is the heart of your application because it determines how the tasks should be displayed and connects the relevant events as required. These functions should be added to the tasksDB object like the majority of the rest of the code discussed in this article.
Listing 5 Loading and displaying tasks
load: function() { var task_list = document.getElementById("task_list"); task_list.innerHTML = ""; if('indexedDB' in window) { var tx = tasksDB.db.transaction(["tasks"], IDBTransaction.READ_WRITE, 0); var objStore = tx.objectStore("tasks"); var cursor = objStore.openCursor(); #1 var i = 0; cursor.onsuccess = function(event) { var result = event.target.result; if(result == null) return; i++; tasksDB.showTask(result.value, task_list); #2 result['continue'](); } tx.oncomplete = function(event) { if(i === 0) { var emptyItem = document.createElement("li"); emptyItem.innerHTML = '<div class="item_title">' +'No tasks to display. <a href="#add">Add one</a>?'; document.getElementById("task_list").appendChild(emptyItem); } } } else if('openDatabase' in window) { tasksDB.db.transaction(function(tx) { tx.executeSql('SELECT * FROM tasks', [], function(tx, results) { var i = 0, len = results.rows.length, list = document.getElementById("task_list"); for(;i<len;i++) { tasksDB.showTask(results.rows.item(i), list); #A } if(len === 0) { var emptyItem = document.createElement("li"); emptyItem.innerHTML = '<div class="item_title">' +'No tasks to display. <a href="#add">Add one</a>?'; document.getElementById("task_list") .appendChild(emptyItem); } }, tasksDB.onerror); }); } }, showTask: function(task, list) { var newItem = document.createElement("li"), checked = ''; var checked = (task.complete == 1) ? ' checked="checked"' : ''; newItem.innerHTML = '<div class="item_complete">' #3 +'<input type="checkbox" name="item_complete" id="chk_' #3 +task.id+'"'+checked+'></div>' #3 + '<div class="item_delete">' #3 + '<a class="lnk_delete" id="del_'+task.id+'">Delete</a></div>' #3 + '<div class="item_title">'+task.desc+'</div>' #3 + '<div class="item_due">'+task.due+'</div>'; #3 list.appendChild(newItem); document.getElementById('del_'+task.id).onclick = function(e) { if(confirm("Are you sure wish to delete this task?", "Confirm delete")) { tasksDB.deleteTask(task.id); } } document.getElementById('chk_'+task.id).onchange = function(e) { var updatedTask = { id: task.id, desc: task.desc, due: task.due, complete: e.target.checked }; tasksDB.updateTask(updatedTask); } }
#1 Uses a cursor to get all tasks
#2 Passes object to showTask
#A Passes object to showTask
#3 Attaches event to new item
Although there is quite a bit of code here, it is relatively straightforward when you consider the work it is doing. The load function is responsible for retrieving the tasks from the IndexedDB or Web SQL database, iterating over the result set and passing each task to the showTask function, which will in turn render the task onto the Task List view in your application. The load function first uses the openCursor API (#1) to retrieve all tasks from the database. It then traverses over each record, passing the task object to the showTask function (#2) for rendering. It uses the cursor’s continue API to move to the next record and exits when there are no records left to render.
The showTask function accepts the task object and a handle to the task list element on the HTML page as arguments, constructs the list item, and adds it to the list accordingly. If the task is marked as complete, this function will set the checkbox to be checked, and it then constructs the various parts of each list item, including the description, due date, checkbox and delete link. Next, the code gets a handle to the new Delete link and attaches the deleteTask function to the click event for the link. Finally, the function gets a handle to the checkbox for the task in question and attaches the updateFunction to the checkbox’s change event (#3).
At this stage, the sample application should be fully functional. Try it out on your iOS or Android device, or indeed on any other desktop or mobile browser that has support for localStorage and either IndexedDB and/or Web SQL. If both IndexedDB and Web SQL are available, the application will favor the former by default.
Summary
In this article, you saw how to open a database connection and create an object store, adding, updating, and deleting records as required. You also learned how to iterate over the task objects in the store using a cursor. In addition to using IndexedDB, the code presented in this article provided a fallback for the HTML5 browsers that have implemented the Web SQL specification but have yet to provide support for IndexedDB.
You might also be interested in:
Quick & Easy HTML5 and CSS3M
Rob Crowther
Sass and Compass in Action
Wynn Netherland, Nathan Weizenbaum, and Chris Eppstein
Secrets of the JavaScript Ninja
John Resig and Bear Bibeault
Opinions expressed by DZone contributors are their own.
Comments