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.

23 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 #

Thanks ! Very nice todo list :D

by osef on March 5, 2012 at 4:06 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 #

Hi
Please where can I download this code?

by mrk1989 on June 22, 2012 at 8:04 pm. Reply #

Hello,

Great job u did here. How would you send data to database ?

Thanks

by Paul on July 17, 2012 at 2:59 pm. Reply #

Fantastic example thanks. Is it possible to have a save button so the ordered list could be saved to a text file ?

Thanks :)

by Tom on July 28, 2012 at 10:28 am. Reply #

Thought you might like to know there is a web designer taking credit for your work at:
http://middleearmedia.com/html5-localstorage/
It’s clear that his came after yours because it is a wordpress blog, and the file system shows that he published it in July 2012:
http://middleearmedia.com/wp-content/uploads/2012/07/html5-localstorage.jpg

by pierre on August 29, 2012 at 8:15 pm. Reply #

It’s awesome! But please, return the demo:(

by Nik on October 23, 2012 at 3:43 pm. Reply #

Hi,

What kind of license did you set up for your todo.js file?

Please let me know as I am eager to use this on my next project.

Thanks,
Sunny

by sunny on October 31, 2012 at 4:08 am. Reply #

http://www.lynda.com/CSS-tutorials/HTML5-Projects-Advanced-Do-List/110281-2.html

They copied exactly what you wrote O_O

by Anonymous on April 1, 2013 at 5:34 am. Reply #

i downloaded the external librarys in the newest version, but it don’t work.

i would be nice if you look for the problem and provide a working zip-file.

Uncaught TypeError: Object [object Object] has no method ‘live’ jquery.inlineedit.js:28
$.fn.inlineEdit jquery.inlineedit.js:28
(anonymous function) base.js:51
fire jquery-1.9.1.js:1037
self.fireWith jquery-1.9.1.js:1148
jQuery.extend.ready jquery-1.9.1.js:433
completed jquery-1.9.1.js:103

by davidak on April 17, 2013 at 4:40 pm. Reply #

Great post. Actually I am looking for exactly this type of shopping list… thanks lot…..

by Billa on April 19, 2014 at 8:05 pm. Reply #

Great example! Exactly what I was looking for. Thank you.

by Anonymous on May 30, 2014 at 3:53 pm. Reply #

This is awesome, thanks! Would it be possible to export/save a list as a text file?

by Tim on June 16, 2015 at 3:40 am. Reply #

This is really amazing !
I just have a slight problem pubsub link is not working :(
I want to use this to do list in my laptop but I still need the last javascript link.

Thank you.

by Emma on March 18, 2017 at 12:23 pm. Reply #

Leave your comment

Required.

Required. Not published.

If you have one.