Sortable and Editable To-do List using HTML5′s localStorage

by koesbong on January 24, 2011

During a job interview I had a couple weeks ago, I was asked to create a to-do list for a coding test with these specifications within 2 hours:

  • Ability to add a new item
  • Ability to remove an item
  • Ability to edit an item
  • Ability to sort an item
  • Save the to-do list using browser’s local storage
  • Third party libraries or plugins are allowed

Long story short, I got everything done except saving the order of the list. It bugged me that I didn’t get to finish it all, so I spent some more time getting it done, polishing it, and here’s the result: Live Demo.

If you’d like to know how it was accomplished, keep on reading.

General Idea

So the general idea on how I approached this is:

  • Each item will be stored into localStorage with they key of “todo-uniqueID
  • The order of the list is tracked by an array, which then gets saved into localStorage with the key of “todo-orders”
  • localStorage with the key of “todo-counter” is used to track the last uniqueID used for the item so that on page refresh, it remembers what the next number should be

Now, the code assumes that you have some basic knowledge of JavaScript and jQuery, so here goes:

The HTML and the CSS

<!doctype HTML>
<html>
    <head>
        <title>CodingTest To-do List</title>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="css/base.css" type="text/css" />
    </head>
    <body>
        <div id="container">
            <h1>CodingTest To-Do List</h1>
            <form id="todo-form">
                <input id="todo" type="text" />
                <input id="submit" type="submit" value="Add to List">
            </form>
            <ul id="show-items"></ul>
            <a href="#" id="clear-all">Clear All</a>
        </div>
        <script src="js/jquery-1.4.4.min.js"></script>
        <script src="js/jquery-ui-1.8.7.custom.min.js"></script>
        <script src="js/jquery.inlineedit.js"></script>
        <script src="js/pubsub.js"></script>
        <script src="js/base.js"></script>
    </body>
</html>

That’s a pretty straight forward HTML. There is an input field where users enter in their to-do item and a button to submit it, a blank ul tag where we will append the added to-do item later on using JavaScript, and a Clear All link to clear the list.

For the script includes, I use jQuery, jQuery UI Sortable, jQuery inlineEdit, pubsub, and base – which is where the magic happens.

You may ask why I use the inlineEdit plugin instead of just using the contenteditable attribute. The reason is because Sortable hijacks the mousedown event thus preventing the editable elements to receive focus when you click on it.

body {
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: 300;
    font-size: 12px;
}
a,
a:link {
    outline: none;
}
h1 {
    font-weight: 100;
    font-size: 72px;
    margin: 20px 0;
}
#container {
    width: 800px;
    text-align: center;
    margin: 20px auto;
}
input[type="text"] {
    height: 30px;
    width: 350px;
    padding: 10px;
    margin-right: 10px;
    font-size: 24px;
}
input[type="submit"] {
    height: 56px;
    padding: 10px;
    border: 1px solid #333;
    background-color: #ccc;
    font-size: 24px;
    cursor: pointer;
    outline: none;
}
ul {
    margin: 15px auto 0;
    padding: 0;
    width: 545px;
}
ul li {
    list-style-type: none;
    font-size: 24px;
    cursor: move;
    background-color: #efefef;
    margin-bottom: 5px;
    padding: 10px;
    text-align: left;
}
ul li span {
    cursor: text;
}
ul li a,
ul li a:link {
    float: right;
    display: none;
    text-decoration: none;
    color: #f03;
}
ul li a:hover {
    text-decoration: underline;
}

The CSS is also pretty straight forward and self-explanatory, nothing too fancy.

The Script – Add a new item

    // Add todo
    var i = Number(localStorage.getItem('todo-counter')) + 1,
          $form = $('#todo-form'),
          $itemList = $('#show-items'),
          $newTodo = $('#todo'),
          order = [];

    $form.submit(function(e) {
        e.preventDefault();
        $.publish('/add/', []);
    });

    $.subscribe('/add/', function() {
        if ($newTodo.val() !== "") {
            // Take the value of the input field and save it to localStorage
            localStorage.setItem(
                "todo-" + i, $newTodo.val()
            );

            // Set the to-do max counter so on page refresh it keeps going up instead of reset
            localStorage.setItem(
                'todo-counter', i
            );

            // Append a new list item with the value of the new todo list
            $itemList.append(
                "<li id='todo-" + i + "'>"
                + "<span class='editable'>"
                + localStorage.getItem("todo-" + i)
                + " </span><a href='#'>x</a></li>"
            );

            $.publish('/regenerate-list/', []);

            // Hide the new list, then fade it in for effects
            $("#todo-" + i).css('display', 'none').fadeIn();

            // Empty the input field
            $newTodo.val("");

            i++;
        }
    });

    $.subscribe('/regenerate-list/', function() {
        var $newTodoList = $('#show-items li');
        // Empty the order array
        order.length = 0;

        // Go through the list item, grab the ID then push into the array
        $newTodoList.each(function() {
            var $this = $(this).attr('id');
            order.push($this);
        });

        // Convert the array into string and save to localStorage
        localStorage.setItem(
            'todo-orders', order.join(',')
        );
    });

Breaking it down: We first declare the variables up top. Next step, we catch the submit event and announce to the app that the form has just been submitted using $.publish(). We then create a $.subscribe() for the add event that actually does all the dirty work.

It first checks to see if the value of the form submit is blank, if not, it retrieves the value then stores it into the localStorage with the key of “todo-uniqueId“, which is whatever the value of i is at that moment. It also stores the value of i into “todo-counter” for uniqueId tracking purposes. Afterward, it appends the new to-do item to the to-do list, displays it appropriately, and empties the input field. We also published “/regenerate-list/” which saves the order of the to-do items. The way that works is it empties the order array, go through the item list elements, get the ID and add to the array, then saving it to “todo-orders”. This “/regenerate-list/” will be used more than once in this app.

The Script – Remove an item

    var $removeLink = $('#show-items li a'),
          $itemList = $('#show-items');

    // Remove todo
    $itemList.delegate("a", "click", function(e) {
        var $this = $(this);

        e.preventDefault();
        $.publish('/remove/', [$this]);
    });

    $.subscribe('/remove/', function($this) {
        var parentId = $this.parent().attr('id');

        // Remove todo list from localStorage based on the id of the clicked parent element
        localStorage.removeItem(
            "'" + parentId + "'"
        );

        // Fade out the list item then remove from DOM
        $this.parent().fadeOut(function() {
            $this.parent().remove();

            $.publish('/regenerate-list/', []);
        });
    });

Breaking it down: As always, variables declarations are up top. $.delegate() is used because we want to catch the click event on the current and the future $removeLink that gets created. “/publish/” then gets announced and what the app does next is it retrieves the ID of the parent of the clicked element and remove the entry in the localStorage based on it. It then fades out the list item element, removes the element from the DOM and publish “/regenerate-list/” to update “todo-orders”.

The Script – Edit and save item

    var $editable = $('.editable');

    // Edit and save todo
    $editable.inlineEdit({
        save: function(e, data) {
                var $this = $(this);
                localStorage.setItem(
                    $this.parent().attr("id"), data.value
                );
            }
    });

Breaking it down: After the variable declaration, we use the $.inlineEdit() call where upon save, it retrieves the ID of the parent of the edited element and re-save the value. Pretty straight forward.

The Script – Reorder and save

    var $itemList = $('#show-items');

    // Sort todo
    $itemList.sortable({
        revert: true,
        stop: function() {
            $.publish('/regenerate-list/', []);
        }
    });

Breaking it down: This is also pretty straight forward. $.sortable() is used to do the sorting functionality and then on the ‘stop’ event, it publishes “/regenerate-list/” to update “todo-orders”.

The Script – Clear all

    var $clearAll = $('#clear-all');

    // Clear all
    $clearAll.click(function(e) {
        e.preventDefault();
        $.publish('/clear-all/', []);
    });

    $.subscribe('/clear-all/', function() {
        var $todoListLi = $('#show-items li');

        order.length = 0;
        localStorage.clear();
        $todoListLi.remove();
    });

Breaking it down: We catch the click event and publish “/clear-all/”, which resets the order array back to 0, clear the localStorage, and remove all list elements from the DOM. Easy peasy.

The Script – The remove button

    var $itemList = $('#show-items');

    // Fade In and Fade Out the Remove link on hover
    $itemList.delegate('li', 'mouseover mouseout', function(event) {
        var $this = $(this).find('a');

        if(event.type === 'mouseover') {
            $this.stop(true, true).fadeIn();
        } else {
            $this.stop(true, true).fadeOut();
        }
    });

Breaking it down: We listen to the mouseover/mouseout event on the list elements and call fadeIn() or fadeOut() appropriately. Also very straight forward.

The Script – Load to-do list

    // Load to-do list
    var orderList,
          j = 0,
          k,
          $itemList = $('#show-items');

    orderList = localStorage.getItem('todo-orders');

    orderList = orderList ? orderList.split(',') : [];

    for( j = 0, k = orderList.length; j < k; j++) {
        $itemList.append(
            "<li id='" + orderList[j] + "'>"
            + "<span class='editable'>"
            + localStorage.getItem(orderList[j])
            + "</span> <a href='#'>X</a></li>"
        );
    }

Breaking it down: After the variable declarations, we set the value of orderList to be the value of the “todo-orders” and convert it into an array. For each item in the array, we create a new list element and retrieve the value of the key using localStorage.getItem().

The Script

$(function() {
    var i = Number(localStorage.getItem('todo-counter')) + 1,
        j = 0,
        k,
        $form = $('#todo-form'),
        $removeLink = $('#show-items li a'),
        $itemList = $('#show-items'),
        $editable = $('.editable'),
        $clearAll = $('#clear-all'),
        $newTodo = $('#todo'),
        order = [],
        orderList;

    // Load todo list
    orderList = localStorage.getItem('todo-orders');

    orderList = orderList ? orderList.split(',') : [];

    for( j = 0, k = orderList.length; j < k; j++) {
        $itemList.append(
            "<li id='" + orderList[j] + "'>"
            + "<span class='editable'>"
            + localStorage.getItem(orderList[j])
            + "</span> <a href='#'>X</a></li>"
        );
    }

    // Add todo
    $form.submit(function(e) {
        e.preventDefault();
        $.publish('/add/', []);
    });

    // Remove todo
    $itemList.delegate('a', 'click', function(e) {
        var $this = $(this);

        e.preventDefault();
        $.publish('/remove/', [$this]);
    });

    // Sort todo
    $itemList.sortable({
        revert: true,
        stop: function() {
            $.publish('/regenerate-list/', []);
        }
    });

    // Edit and save todo
    $editable.inlineEdit({
        save: function(e, data) {
                var $this = $(this);
                localStorage.setItem(
                    $this.parent().attr("id"), data.value
                );
            }
    });

    // Clear all
    $clearAll.click(function(e) {
        e.preventDefault();
        $.publish('/clear-all/', []);
    });

    // Fade In and Fade Out the Remove link on hover
    $itemList.delegate('li', 'mouseover mouseout', function(event) {
        var $this = $(this).find('a');

        if(event.type === 'mouseover') {
            $this.stop(true, true).fadeIn();
        } else {
            $this.stop(true, true).fadeOut();
        }
    });

    // Subscribes
    $.subscribe('/add/', function() {
        if ($newTodo.val() !== "") {
            // Take the value of the input field and save it to localStorage
            localStorage.setItem(
                "todo-" + i, $newTodo.val()
            );

            // Set the to-do max counter so on page refresh it keeps going up instead of reset
            localStorage.setItem('todo-counter', i);

            // Append a new list item with the value of the new todo list
            $itemList.append(
                "<li id='todo-" + i + "'>"
                + "<span class='editable'>"
                + localStorage.getItem("todo-" + i)
                + " </span><a href='#'>x</a></li>"
            );

            $.publish('/regenerate-list/', []);

            // Hide the new list, then fade it in for effects
            $("#todo-" + i)
                .css('display', 'none')
                .fadeIn();

            // Empty the input field
            $newTodo.val("");

            i++;
        }
    });

    $.subscribe('/remove/', function($this) {
        var parentId = $this.parent().attr('id');

        // Remove todo list from localStorage based on the id of the clicked parent element
        localStorage.removeItem(
            "'" + parentId + "'"
        );

        // Fade out the list item then remove from DOM
        $this.parent().fadeOut(function() {
            $this.parent().remove();

            $.publish('/regenerate-list/', []);
        });
    });

    $.subscribe('/regenerate-list/', function() {
        var $todoItemLi = $('#show-items li');
        // Empty the order array
        order.length = 0;

        // Go through the list item, grab the ID then push into the array
        $todoItemLi.each(function() {
            var id = $(this).attr('id');
            order.push(id);
        });

        // Convert the array into string and save to localStorage
        localStorage.setItem(
            'todo-orders', order.join(',')
        );
    });

    $.subscribe('/clear-all/', function() {
        var $todoListLi = $('#show-items li');

        order.length = 0;
        localStorage.clear();
        $todoListLi.remove();
    });
});

Wrapping up

Once again, you can see the live demo here. I have learned a lot through this project and hopefully can be beneficial for someone out there.

I’d like to thank Nithin Bekal whose code got me started thinking on this project, Nathan Ostgard for the mid-day and late night code help, as well as Josh Cody for code optimization help.

9 comments

Are you kidding me? They wanted you to do all this in 2 hours? WOW! Insane.

Anyway… I love what you did here. I’m trying to do something a little more complex at work and this really helped me A LOT!

Thanks for sharing this!

by Joel on January 30, 2011 at 3:12 am. Reply #

Thx for this great post!

by Hansen on February 19, 2011 at 7:09 am. Reply #

[...] also spent time to enhance it more – by adding sorting capability (based on the codebase from this tutorial), status change of each task ( active/inactive), extra note field and some other small [...]

by TO.DO.TO – playing with HTML5 localStorage - flisterz:blog on February 21, 2011 at 3:30 pm. Reply #

I hope you got the job! Of all the to-do lists that I’ve found so far, this is the one that does exactly what I’d been looking for. Thanks heaps for sharing.

by Giovanni on May 11, 2011 at 12:28 am. Reply #

On an iPad the delete link is present when you edit, which can lead to an inadvertant deletion.

This is because the hover event doesn’t exist on the iPad.

by John Moreno on May 29, 2011 at 10:03 am. Reply #

Worth to bookmark!

by khensolomon on September 4, 2011 at 11:32 am. Reply #

Thanks for sharing this! I ‘ve been searching for a solution all day yesterday with no result! I hope you get that job!

by forbdn on November 23, 2011 at 11:50 am. Reply #

Great post ! nice job

by WebDev of World on December 11, 2011 at 5:20 pm. Reply #

This is Great! Super helpful. I’m finally making the switch over from years of Flash development to HTML5 and as always grateful for the community.

I’m trying to build something similar, but need to create multiple lists that you can give unique names and drag and drop between the two. Any suggestions or tips for how to accomplish?

Thanks!

by barrett paul on April 19, 2012 at 1:06 am. Reply #

Leave your comment

Required.

Required. Not published.

If you have one.