Sunday 26 January 2014

Creating a Todo list using Indexed DB and Promise


What is Indexed DB

HTML5 brought lots of capabilities to the browsers including some storage APIs. One of the storage options available on browsers today is Indexed DB. As the name itself suggests, Indexed DB is much like a database, but it is meant to store JavaScript objects. Data in the Indexed DB is not relational; it just has to follow a definite structure.

The browsers support a number of APIs to help us interact with the Index DB. Details about all of the Indexed DB API can be found on the Mozilla Developer Network. Current implementation of the APIs by the browsers may not be perfect, as most of the APIs are still under development. But, the browsers have enough support now so that we can play around. Check caniuse.com to find if your browser supports Indexed DB.

As API of the Indexed DB says, all operations performed over Indexed DB are executed asynchronously. The operations don’t let the current thread processing to wait unless the operation is finished. Instead, we need to hook-up call backs to perform actions upon success or failure of the operation.

Promises in the browser

In the latest version of JavaScript, the language got built-in support for promises. This means, we don’t need to use any of the third party libraries to prettify the asynchronous code and keep it clean from nasty call backs. As Indexed DB works asynchronously, we can use promise API to make the Indexed DB operations cleaner. Very few of the browsers support the promise API as of now. Hopefully others will join the club soon.

Using the promise API is very easy. But you need to have a good understanding of what promises are before proceeding further. Following snippet shows how to use the promise API in an asynchronous function:


function operate(){
    var promise = new Promise(resolve, reject){
        if(<some condition>){
     resolve(data);
 }
 else{
     reject(error);
 }
    };
    return promise;
}


Success and failure callbacks can be hooked up to the promise object returned in the above function. Following snippet shows this:

operate().then(function(data){
    //Update UI
}, function(error){
    //Handle the error
});


Building a todo list app

Let’s start implementing a simple todo list using Indexed DB. To store the todo items in the Indexed DB, we need to create an object store. While creating, we need to specify the version of the DB and the key property. Any object inserted into the object store must contain the key property. Following code shows how to create a DB store:


var open = function(){
    var version = 1;
    var promise = new Promise(function(resolve, reject){
        //Opening the DB
        var request = indexedDB.open("todoData", version);      

        //Handling onupgradeneeded
        //Will be called if the database is new or the version is modified
        request.onupgradeneeded = function(e) {
            db = e.target.result;
            e.target.transaction.onerror = indexedDB.onerror;

            //Deleting DB if already exists
            if(db.objectStoreNames.contains("todo")) {
                db.deleteObjectStore("todo");
            }
            //Creating a new DB store with a paecified key property
            var store = db.createObjectStore("todo",
                {keyPath: "id"});
        };

        //If opening DB succeeds
        request.onsuccess = function(e) {
            db = e.target.result;
            resolve();
        };

        //If DB couldn't be opened for some reason
        request.onerror = function(e){
            reject("Couldn't open DB");
        };
    });
    return promise;
};


Now that the DB is opened, let’s extract all todo items (if exist) from it. The logic of fetching the items is conceptually similar to the way we do it with databases. Following are the steps:

  1. Open a cursor for the request to fetch items from lower bound
  2. The items will be returned one by one. Handle appropriate call back and consolidate all the values
Following code achieves this:

var getAllTodos = function() {
    var todosArr = [];

    //Creating a transaction object to perform Read/Write operations
    var trans = db.transaction(["todo"], "readwrite");
        
    //Getting a reference of the todo store
    var store = trans.objectStore("todo");

    //Wrapping all the logic inside a promise
    var promise = new Promise(function(resolve, reject){
        //Opening a cursor to fetch items from lower bound in the DB
        var keyRange = IDBKeyRange.lowerBound(0);
        var cursorRequest = store.openCursor(keyRange);

        //success callback
        cursorRequest.onsuccess = function(e) {
            var result = e.target.result;
                
            //Resolving the promise with todo items when the result id empty
            if(result === null || result === undefined){
                resolve(todosArr);
            }
            //Pushing result into the todo list 
            else{
                todosArr.push(result.value);
                if(result.value.id > lastIndex){
                    lastIndex=result.value.id;
                }
                result.continue();
            }
        };

        //Error callback
        cursorRequest.onerror = function(e){
            reject("Couldn't fetch items from the DB");
        };
    });
    return promise;
};


To add a new item to the todo list, we need to invoke the put method on the store object. As mentioned earlier, key property should be present and it should be assigned with a unique value. So, we calculate value of the next id and assign it to the object.
var addTodo = function(todoText) {
    //Creating a transaction object to perform read-write operations
    var trans = db.transaction(["todo"], "readwrite");
    var store = trans.objectStore("todo");
    lastIndex++;
        
    //Wrapping logic inside a promise
    var promise = new Promise(function(resolve, reject){
        //Sending a request to add an item
        var request = store.put({
            "id": lastIndex,
            "text": todoText
        });
            
        //success callback
        request.onsuccess = function(e) {
            resolve();
        };
            
        //error callback
        request.onerror = function(e) {
            console.log(e.value);
            reject("Couldn't add the passed item");
        };
    });
    return promise;
};


Finally, let’s delete todo item from the list. The logic remains quite similar to that of adding a new todo. The only difference is, we need just id of the item to be deleted.
var deleteTodo = function(id) {                                                                                     
    var promise = new Promise(function(resolve, reject){  
        var trans = db.transaction(["todo"], "readwrite");
        var store = trans.objectStore("todo");            
        var request = store.delete(id);                   
                                                          
        request.onsuccess = function(e) {                 
            resolve();                                    
        };                                                
                                                          
        request.onerror = function(e) {                   
            console.log(e);                               
            reject("Couldn't delete the item");           
        };                                                
    });                                                   
    return promise;                                       
};


Code of the complete sample is available at this plunk: http://plnkr.co/edit/aePFAaCucAKOXbb1qL85?p=preview

Happy coding!

No comments:

Post a Comment

Note: only a member of this blog may post a comment.