Creating a Notepad app with indexedDB

Sandeep Panda

indexedDB, which is new in HTML5, allows developers to persist data within the web browser. As a result your app runs both online and offline with powerful query facilities. indexedDB is different from traditional relational databases in that it is an objectStore instead of a collection of rows and columns. You just create an objectStore in indexedDB and store JavaScript objects in that store. Furthermore, it’s very easy to perform CRUD operations on your stored objects. This tutorial gives an overview of indexedDB API and explains how to use this to create a simple notepad application.

Before we get started note that indexedDB API specification has not stabilized yet. But if you have the latest Firefox or Google Chrome installed on your machine you are good to go. To know which browser versions support the API see the compatibility table.

There are two types of APIs in indexedDB spec: synchronous and asynchronous. However, we will focus on the asynchronous API as currently this is the only API that’s supported by browsers. Asynchronous means you perform an operation on the database and receive the result in a callback through a DOM event.

In any note-making app there are four simple functions: Create, Read, Update and Delete. indexedDB provides very simple APIs to perform these operations. But before doing anything we need to create a database and open it.

Setting Up:

Since the specification has not stabilized yet, different browsers use prefixes in their implementations. So, you need to check correctly to ensure that a browser supports indexedDB. Use the following code to ensure that a browser supports indexedDB.

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;
if (!window.indexedDB) {
    alert("Sorry!Your browser doesn't support IndexedDB");
}

Opening/Creating a Database:

Opening an indexedDB database is a matter of calling indexedDB.open().

var request = window.indexedDB.open("notepad",1);

indexedDB.open() accepts two parameters. The first one represents the database name. If the database does not already exist it creates a new one. The second parameter is the version number. If you need to update the database schema in future you need to call indexedDB.open()function with a version number higher than the previous one. In that case you will need to implement onupgradeneeded callback where you can update the database schema and create/delete objectStores.

Creating the database is the first step. But to store something you need to have an objectStore. When you create a database, you will probably want to create an objectStore as well. When a database is initially created onupgradeneeded callback is fired where you can create an objectStore.

var database;

var request = window.indexedDB.open("notepad",1);

request.onerror = function(event) {

console.log(event.target.errorCode);

};

request.onsuccess = function(event) {

    database=request.result;

};

request.onupgradeneeded = function(event) {

    var db = event.target.result;

    var objectStore = db.createObjectStore("notes", { keyPath:  "id",autoIncrement:true});

};

In the above code sample we call indexedDB.open() with database name notepad and version number 1. The method returns an  IDBOpenDBRequest. When the database open request succeeds the request.onsuccess callback is fired. The result property of request is an instance of IDBDatabase which we assign to variable database for later use.

Inside onupgradeneeded callback we get a reference to the database and use it to create a new objectStore called notes. createObjectStore() function also accepts a second parameter.  In this case we have defined a keyPath called id that uniquely identifies an object in our store. In addition we also want the id to be autoIncrementing.

Adding/Updating an Item in the objectStore:

Let’s say we want to persist a note in the store. The object should have fields like: title, body and a creation date. To store the object use the following code:

var note={title:”Test Note”, body:”Hello World!”, date:”01/04/2013”};

var transaction = database.transaction(["notes"], "readwrite");

var objectStore = transaction.objectStore("notes");

var request=objectStore.put(note);

request.onsuccess = function(event) {

    //do something here

};

database.transaction() takes an array as first parameter which represents the names of objectStores this transaction spans. The second parameter determines the type of transaction. If you don’t pass a second argument you will get a read only transaction. Since we want to add a new item we pass readwrite as the second argument. As a result of this call we get a transaction object. transaction.objectStore() selects an objectStore to operate on.   Finally objectStore.put() adds the object to the store. You can also use objectStore.add() to add an object. But the former will update an object in the store if we attempt to add a new object with an id the same as the id of an existing entry.

Deleting an Item from the store:

Deleting an object from the store is pretty simple and straightforward.

var request = database.transaction(["notes"], "readwrite") .objectStore("notes").delete(20);

request.onsuccess = function(event) {

    //handle success

};

The above code deletes an object from the store which has an id of 20.

Querying all objects in store:

For any database driven app it’s very common to display all the stored entries.  In indexedDB you can get the objects stored in the store and iterate through them with the help of a cursor.

var objectStore = database.transaction("notes").objectStore("notes");

objectStore.openCursor().onsuccess = function(event) {

    var cursor = event.target.result;

    if (cursor) {

        alert("Note id: "+cursor.key+", Title: "+cursor.value.title);

        cursor.continue();

    }

};

The above code is very simple to understand.  The openCursor() function can accept several arguments. You can control the range of results returned and the direction of iteration by passing appropriate parameters. The cursor object is the result of the request. cursor.continue() should be called if you expect multiple objects and want to iterate through them. This means as long as there is more data onsuccess callback is fired, provided you call cursor.continue().

So, this is all you should know before developing the notepad app using indexedDB.  Now, I will show how to create the app step by step.

Initial HTML markup:

<html>

<head><title>Simple Notepad Using indexedDB</title>

</head>

<body>

<div id="container">

    <h3 id="heading">Add a note</h3>

    <input type="hidden" value="0" id="flag"/>

    <a href="#" id="add"><img src="add.png" onclick="createNote(0)"/> New</a>

    <a href="#" id="back"><img src="back.png" onclick="goBack()"/></a>

    <div id="notes"></div>

    <div id="editor" contenteditable="true"></div>

</div>

</body>

</html>

Explanation:

We have two divs: notes and editor. The first one is used for showing a list of existing notes and the second one is used as an editor to write a note.  The editor div is initially invisible. When the user clicks on the add button we hide the notes div and show the editor div. You should keep in mind that by setting contenteditable="true" we are making a div editable. We also have a hidden input field with id flag. This will be used later in the tutorial.

The JavaScript:

<script type="text/javascript">

var database;

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;

if (!window.indexedDB) {

    alert("Sorry!Your browser doesn't support IndexedDB");

}

function init(){

    var request = window.indexedDB.open("notepad",1);

    request.onerror = function(event) {

    console.log(event.target.errorCode);

};

request.onsuccess = function(event) {

    database=request.result;

    showNotes();

};

request.onupgradeneeded = function(event) {

    var db = event.target.result;

    var objectStore = db.createObjectStore("notes", { keyPath: "id",autoIncrement:true});

    };

}

function createNote(id){

    document.getElementById("editor").style.display="block";

    document.getElementById("editor").focus();

    document.getElementById("back").style.display="block";

    document.getElementById("add").style.display="none";

    document.getElementById("notes").style.display="none";

    if(parseInt(id)!=0){

    database.transaction("notes").objectStore("notes").get(parseInt(id))

    .onsuccess = function(event) {

document.getElementById("editor").innerHTML=event.target.result.body;

    document.getElementById("flag").value=id;

};

}

}

function goBack(){

    var note={};

    note.body=document.getElementById("editor").innerHTML;

    note.title=getTitle(note.body);

    note.date=getDate();

    var flag=parseInt(document.getElementById("flag").value);

    if(flag!=0)

      note.id=flag;

    if(note.title.trim()==="")

        window.location.href="index.html";

    else

        addNote(note);

    }

function getDate(){

    var date=new Date();

    var month=parseInt(date.getMonth())+1;

    return date.getDate()+"/"+month+"/"+date.getFullYear();

}

function getTitle(body){

    var body = body.replace(/(<([^>]+)>)/ig,"");

    if(body.length > 20) body = body.substring(0,20)+". . .";

        return body;

}

function addNote(note){

    var transaction = database.transaction(["notes"], "readwrite");

    var objectStore = transaction.objectStore("notes");

    var request=objectStore.put(note);

    request.onsuccess = function(event) {

        document.getElementById("flag").value="0";

        window.location.href="index.html";

        };

    }

function showNotes(){

var notes="";

var objectStore = database.transaction("notes").objectStore("notes");

objectStore.openCursor().onsuccess = function(event) {

    var cursor = event.target.result;

    if (cursor) {

        var link="<a class="notelist" id=""+cursor.key+"" href="#">"+cursor.value.title+"</a>"+" 
        <img class="delete" src="delete.png" height="30px" id=""+cursor.key+""/>";

        var listItem="<li>"+link+"</li>";

        notes=notes+listItem;

        cursor.continue();

    }

    else

    {

    document.getElementById("notes").innerHTML="<ul>"+notes+"</ul>";

    registerEdit();

    registerDelete();

    }

};

}

function deleteNote(id){

var request = database.transaction(["notes"], "readwrite")

        .objectStore("notes")

        .delete(id);

request.onsuccess = function(event) {

    window.location.href="index.html";

};

}

function registerEdit(){

var elements = document.getElementsByClassName('notelist');

for(var i = 0, length = elements.length; i < length; i++) {

    elements[i].onclick = function (e) {

        createNote(this.id);

    }

}

}

function registerDelete(){

var deleteButtons = document.getElementsByClassName('delete');

    for(var i = 0, length = deleteButtons.length; i < length; i++){

        deleteButtons[i].onclick=function(e){

        deleteNote(parseInt(this.id));

        }

    }

}

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

</script>

Explanation:

The init method does the necessary initialization. It creates/opens the database and also creates an objectStore when the database is first created.  After the database is successfully opened we get a reference to it and store it in a database variable.

showNotes() function displays a list of notes created by the user. We start a transaction and get the note objects that are present in the store. Then we create an unordered list of the note titles and finally display it in the div having id notes. We also call two functions registerEdit() and registerDelete(). The first function attaches a click event listener to the note titles which are simple links having class notelist so that the notes can be edited when someone clicks on the title. The latter function adds a click event listener to the delete buttons (simple images) that are present beside the note titles. By doing this we can delete a note when someone clicks on delete button. The deleteNote() function performs the delete operation.

The createNote() function displays an editor to create a new note or update an existing one . It accepts one argument. If it’s 0, we know that we want to create a new note. Otherwise we start a transaction to get the content of an existing note. We pass the id of the note to objectStore.get() and in onsuccess we fetch the body of the note. Then we populate the editor with the fetched note body. In addition we set our hidden input flag to the id which is used in goBack() function.  This method is fired when the user wants to go back after writing a note. This is where we save a note in the store.

In the goBack() function we create a new object and set its title, body and date property. The title is taken as the first 20 characters of the body. Then find out the value of hidden flag. If it’s not 0 we know we want to update an existing note. Therefore, we set the id property of the created object. Otherwise there is no need of an id property as the object is going to be a new entry in the store. In the end addNote() function is called with the note object as argument. The addNote() function simply starts a transaction that adds/updates an object in the store. If the transaction succeeds we take the user back where he/she can see a list of created notes.

You can try out the demo app here. I have tested the app in Chrome 25 and Firefox 20.

indexedDB is a great API in HTML5 and when used with app cache can be very powerful. Mozilla has some interesting and very useful information about indexedDB. Do check them out if you want to know more.

If you are not able to get something let me know in the comments.

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

No Reader comments

Comments on this post are closed.