JavaScript Web Storage Tutorial: Creating an Address Book Application

Web Storage (or DOM Storage) represents a mechanism for persisting data on the client. This hands-on tutorial describes how to use the Web Storage API by creating a simple address book application.

The application consists of a table to list all the entries and a form to add new entries or edit existing entries stored in the local storage area. Below is a screenshot of the completed application as it appears in a browser:

Each contact entry consists of three fields, first name, last name and e-mail address, and is identified by an unique ID. Below is an example of such an object using the JavaScript literal notation:

{
    id: 1,
    first_name: "John",
    last_name: "Smith",
    email: "john@example.com"
}

The Markup

We'll begin by creating the necessary markup for the table and the form. The form will be used for both adding and editing, thus we need a hidden field to store the current object's ID (initialized to 0). The table has an extra column for the actions links.

<h1>Contacts</h1>
<table id="contacts-table">
    <tr id="contacts-head">
        <th>ID</th>
        <th>First name</th>
        <th>Last name</th>
        <th>Email</th>
        <th>Actions</th>
    </tr>
</table>

<form id="contacts-form">
    <div class="item text">
        <label>First name:</label>
        <div class="field"><input type="text" name="first_name" /></div>
    </div>
    <div class="item text">
        <label>Last name:</label>
        <div class="field"><input type="text" name="last_name" /></div>
    </div>
    <div class="item text">
        <label>Email:</label>
        <div class="field"><input type="text" name="email" /></div>
    </div>
    <div class="button-wrapper">
        <div class="item button">
            <div class="field"><input type="button" id="contacts-op-discard" value="Discard" /></div>
        </div>
        <div class="item button button-default">
            <div class="field"><input type="submit" id="contacts-op-save" value="Save" /></div>
        </div>
    </div>
    <input type="hidden" name="id_entry" value="0" />
</form>

To build the application, we'll encapsulate the logic inside an object and expose only specific properties and methods, such as for adding or removing entries to and from the data store or the table:

var Contacts = {
    index: 1,

    init: function() {},

    storeAdd: function(entry) {},
    storeEdit: function(entry) {},
    storeRemove: function(entry) {},

    tableAdd: function(entry) {},
    tableEdit: function(entry) {},
    tableRemove: function(entry) {}
};
Contacts.init();

The init() method initializes the application. The storeAdd(), storeEdit() and storeRemove() methods are used for adding, editing and removing entries to and from the storage area, while the tableAdd(), tableEdit() and tableRemove() are used to add, update and remove entries to and from the table.

Before implementing these methods, let's take a look at the way our entries will be stored in the local storage:

  • each entry is uniquely identified by an ID generated using an auto incrementing field named index that will also be stored in the local storage for use in later sessions
  • keys are prefixed with the word "Contacts" to avoid naming conflicts with other applications/scripts that store information in the local storage on the same domain
  • entries are stored as strings serialized in the JSON format

The following table illustrates this:

Key:Value:
Contacts:1{"id":1,"first_name":"John","last_name":"Smith","email":"john@example.com"}
Contacts:index2

The initial value of the index property is set to 1. By incrementing it after adding a new entry, we will generate a unique identity for the next entry. In other words, the index property holds the ID of the next entry.

Initializing The Application

The Contact.init() method initializes the storage index, sets up the form and populates the table with existing entries:

var Contacts = {
    index: window.localStorage.getItem("Contacts:index"),
    $table: document.getElementById("contacts-table"),
    $form: document.getElementById("contacts-form"),
    $button_save: document.getElementById("contacts-op-save"),
    $button_discard: document.getElementById("contacts-op-discard"),

    init: function() {
        // initialize the storage index
        if (!Contacts.index) {
            window.localStorage.setItem("Contacts:index", Contacts.index = 1);
        }

        // initialize the form
        ...

        // initialize the table
        ...
    },
    ...
};

Setting up the form

Initializing the form simply means adding event listeners to the Discard and Save buttons:

var Contacts = {
    ...
    init: function() {
        ...
        // initialize the form
        Contacts.$form.reset();
        Contacts.$button_discard.addEventListener("click", function(event) {
        	Contacts.$form.reset();
        	Contacts.$form.id_entry.value = 0;
        }, true);
        Contacts.$form.addEventListener("submit", function(event) {
            var entry = {
                id: parseInt(this.id_entry.value),
                first_name: this.first_name.value,
                last_name: this.last_name.value,
                email: this.email.value
            };
            if (entry.id == 0) { // add
                Contacts.storeAdd(entry);
                Contacts.tableAdd(entry);
            }
            else { // edit
                Contacts.storeEdit(entry);
                Contacts.tableEdit(entry);
            }

            this.reset();
            this.id_entry.value = 0;
            event.preventDefault();
        }, true);
        ...
    },
    ...
};

Since we're using the form for both adding and editing, every time a user submits the form we need to check whether we're dealing with new or existing entries. Every time we add a new entry or edit an existing one, we need to update both the local storage area and the table.

Populating the table

To populate the table with entries, we simply iterate over each item in the localStorage object, test if their key is valid and finally add new rows to the table:

var Contacts = {
    ...
    init: function() {
        ...
        // initialize the table
        if (window.localStorage.length - 1) {
            var contacts_list = [], i, key;
            for (i = 0; i < window.localStorage.length; i++) {
                key = window.localStorage.key(i);
                if (/Contacts:\d+/.test(key)) {
                    contacts_list.push(JSON.parse(window.localStorage.getItem(key)));
                }
            }

            if (contacts_list.length) {
                contacts_list
                    .sort(function(a, b) {
                        return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0);
                    })
                    .forEach(Contacts.tableAdd);
            }
        }
    },
    ...
};

A valid key is a key that starts with Contacts: and ends with an integer. Items identified by keys that don't match that pattern are ignored.

Keys in the localStorage are retrieved by their index using the key() method. Values associated with those keys are returned by the getItem() method.

Adding new entries

Now that the application is initialised, let's add some entries to both the local storage and the table:

var Contacts = {
    ...
    storeAdd: function(entry) {
        entry.id = Contacts.index;
        window.localStorage.setItem("Contacts:"+ entry.id, JSON.stringify(entry));
        window.localStorage.setItem("Contacts:index", ++Contacts.index);
    },
    ...

    tableAdd: function(entry) {
        var $tr = document.createElement("tr"), $td, key;
        for (key in entry) {
            if (entry.hasOwnProperty(key)) {
                $td = document.createElement("td");
                $td.appendChild(document.createTextNode(entry[key]));
                $tr.appendChild($td);
            }
        }
        $td = document.createElement("td");
        $td.innerHTML = '<a data-op="edit" data-id="'+ entry.id +'">Edit</a> | <a data-op="remove" data-id="'+ entry.id +'">Remove</a>';
        $tr.appendChild($td);
        $tr.setAttribute("id", "entry-"+ entry.id);
        Contacts.$table.appendChild($tr);
    },
    ...
};

The storeAdd() method saves the entry in the local storage area by calling the setItem() method of the localStorage object. The tableAdd() method creates a new table row and appends it to the table.

Updating and deleting entries

The Edit and Remove links are added in the last column. Instead of adding event listeners for each action link, we're going to add an event listener on the table and then determine in the callback function which action was triggered. This is a technique known as event delegation:

var Contacts = {
    ...
    init: function() {
        ...
        // initialize the table
        ...
        Contacts.$table.addEventListener("click", function(event) {
            var op = event.target.getAttribute("data-op");
            if (/edit|remove/.test(op)) {
                var entry = JSON.parse(window.localStorage.getItem("Contacts:"+ event.target.getAttribute("data-id")));
                if (op == "edit") {
                    Contacts.$form.first_name.value = entry.first_name;
                    Contacts.$form.last_name.value = entry.last_name;
                    Contacts.$form.email.value = entry.email;
                    Contacts.$form.id_entry.value = entry.id;
                }
                else if (op == "remove") {
                    if (confirm('Are you sure you want to remove "'+ entry.first_name +' '+ entry.last_name +'" from your contacts?')) {
                        Contacts.storeRemove(entry);
                        Contacts.tableRemove(entry);
                    }
                }
                event.preventDefault();
            }
        }, true);
    },
    ...
};

To determine what action to perform and on what entry, we're relying on the data-op and data-id attributes of the action links.

Updating Existing Entries

var Contacts = {
    ...
    storeEdit: function(entry) {
        window.localStorage.setItem("Contacts:"+ entry.id, JSON.stringify(entry));
    },
    ...

    tableEdit: function(entry) {
        var $tr = document.getElementById("entry-"+ entry.id), $td, key;
        $tr.innerHTML = "";
        for (key in entry) {
            if (entry.hasOwnProperty(key)) {
                $td = document.createElement("td");
                $td.appendChild(document.createTextNode(entry[key]));
                $tr.appendChild($td);
            }
        }
        $td = document.createElement("td");
        $td.innerHTML = '<a data-op="edit" data-id="'+ entry.id +'">Edit</a> | <a data-op="remove" data-id="'+ entry.id +'">Remove</a>';
        $tr.appendChild($td);
    },
    ...
};

The storeEdit() method saves the new entry over the existing one using the setItem() method of the localStorage object. Note that the setItem() method simply sets a key/value combination. If the key already exists, the associated value is replaced with the new value.

The tableEdit() method empties the existing table row and creates new cells with the new values.

Deleting Existing Entries

Finally, entry removal is accomplished by removing the corresponding item from the localStorage and the corresponding table row:

var Contacts = {
    ...
    storeRemove: function(entry) {
        window.localStorage.removeItem("Contacts:"+ entry.id);
    },
    ...

    tableRemove: function(entry) {
        Contacts.$table.removeChild(document.getElementById("entry-"+ entry.id));
    }
};

The removeItem() method of the localStorage object removes the item with the specified key.

And that's it! You can see the complete application here.

See also

If you see a typo, want to make a suggestion or have anything in particular you'd like to know more about, please drop us an e-mail at hello at diveintojavascript dot com.

Copyright © 2010-2013 Dive Into JavaScript