Monday, 27 January 2014

Creating a Todo List using Indexed DB and Angular JS

In last post, we saw how to use Indexed DB with promise API implemented inside the browsers. In this post, we will rewrite the same sample using Angular JS. So, instead of using promise API of the browser, we will use Angular’s $q, as it makes the data binding system happy. And instead of performing the CRUD operations on Indexed DB inside a revealing module, we will do it inside a factory.

Indexed DB is available to the global scope. This means, it is a property of the window object. The best practice to use it in a factory is through the $window object, as it is injectable and makes the factory testable. Following snippet shows first few statements of the factory:

var app = angular.module('indexDBSample', []);
app.factory('indexedDBDataSvc', function($window, $q){
  var indexedDB = $window.indexedDB;
  var db=null;
  var lastIndex=0;
  ....
  ....
  ....
});


We need to add methods to open DB, get todo items, add new item and delete an item just as we did in the last post. Logic in the methods remains the same, except usage of the promise.

The open method opens the database, checks for upgrade and handles the call back if it needs. It specifies the key property of the database while creating a new database.

var open = function(){
  var deferred = $q.defer();
  var version = 1;
  var request = indexedDB.open("todoData", version);
  request.onupgradeneeded = function(e) {
    db = e.target.result;
    e.target.transaction.onerror = indexedDB.onerror;
    if(db.objectStoreNames.contains("todo")) {
      db.deleteObjectStore("todo");
    }
    var store = db.createObjectStore("todo",
      {keyPath: "id"});
  };
  request.onsuccess = function(e) {
    db = e.target.result;
    deferred.resolve();
  };
  request.onerror = function(){
    deferred.reject();
  };
  
  return deferred.promise;
};


The getTodos method fetches all available items in the DB and resolves promise with the results once all results are obtained. The fetch operation is performed using a cursor request, which returns the items individually from the indexed DB.

var getTodos = function(){
  var deferred = $q.defer();
  
  if(db === null){
    deferred.reject("IndexDB is not opened yet!");
  }
  else{
    var trans = db.transaction(["todo"], "readwrite");
    var store = trans.objectStore("todo");
    var todos = [];
  
    // 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 === null || result === undefined)
      {
        deferred.resolve(todos);
      }
      else{
        todos.push(result.value);
        if(result.value.id > lastIndex){
          lastIndex=result.value.id;
        }
        result.continue();
      }
    };
  
    cursorRequest.onerror = function(e){
      console.log(e.value);
      deferred.reject("Something went wrong!!!");
    };
  }
  
  return deferred.promise;
};


The addTodo method generated the next value for the key column and adds it to the Indexed DB.

var addTodo = function(todoText){
  var deferred = $q.defer();
  
  if(db === null){
    deferred.reject("IndexDB is not opened yet!");
  }
  else{
    var trans = db.transaction(["todo"], "readwrite");
    var store = trans.objectStore("todo");
    lastIndex++;
    var request = store.put({
      "id": lastIndex,
      "text": todoText
    });
  
    request.onsuccess = function(e) {
      deferred.resolve();
    };
  
    request.onerror = function(e) {
      console.log(e.value);
      deferred.reject("Todo item couldn't be added!");
    };
  }
  return deferred.promise;
};


Finally, the deleteTodo method accepts value of key property of the item to be deleted and invokes the delete method of the store to delete the item.

var deleteTodo = function(id){
  var deferred = $q.defer();
  
  if(db === null){
    deferred.reject("IndexDB is not opened yet!");
  }
  else{
    var trans = db.transaction(["todo"], "readwrite");
    var store = trans.objectStore("todo");
  
    var request = store.delete(id);
  
    request.onsuccess = function(e) {
      deferred.resolve();
    };
  
    request.onerror = function(e) {
      console.log(e.value);
      deferred.reject("Todo item couldn't be deleted");
    };
  }
  
  return deferred.promise;
};


Implementation of the controller is pretty straight forward. The controller invokes the members of the factory and updates the data items that are used to bind data on the UI.

app.controller('TodoController', function($window, indexedDBDataSvc){
  var vm = this;
  vm.todos=[];
  
  vm.refreshList = function(){
    indexedDBDataSvc.getTodos().then(function(data){
      vm.todos=data;
    }, function(err){
      $window.alert(err);
    });
  };
  
  vm.addTodo = function(){
    indexedDBDataSvc.addTodo(vm.todoText).then(function(){
      vm.refreshList();
      vm.todoText="";
    }, function(err){
      $window.alert(err);
    });
  };
  
  vm.deleteTodo = function(id){
    indexedDBDataSvc.deleteTodo(id).then(function(){
      vm.refreshList();
    }, function(err){
      $window.alert(err);
    });
  };
  
  function init(){
    indexedDBDataSvc.open().then(function(){
      vm.refreshList();
    });
  }
  
  init();
});




The complete sample is available on this plunk: http://plnkr.co/edit/7oSOUHC9hSnD8d6COkSK?p=preview

Happy coding!

3 comments:

  1. Excellent tutorial. Keep up the good work.

    ReplyDelete
  2. Is there a specific reason as to why the example is invoking a service directly instead of following the delegation route - view -> controller -> service -> factory?

    I mean shouldn't it be the conventional $scope.addTodo = function (){...};


    ReplyDelete