How to build a TODO App with Knockout and Mobiscroll

We created a couple of demo apps that are using Mobiscroll components and our version of a TODO App is one of them.
See all demo apps and kitchen sink demos here.

Besides writing demos focused on specific features and configurations we wanted to tackle a “real-life” scenario. So the the app focusing on daily todo’s came about.
This breakdown – if you will – is to illustrate how we implemented the app with Knockout using two Mobiscroll controls, the Calendar and the Listview.
A basic knowledge of Knockout is necessary to fully understand the implementation, however implementing a similar Todo app without knockout is possible as well.
For more on how to get started with Knockout please visit Learn Knockout.

Todo App

In this demo app we used the Model-View-View Model (MVVM) design pattern and Knockout was our framework of choice. MVVM lets us completely decouple our app logic and data from the UI, so maintenance and changes are super-easy to make. Beside that the UI updates are automatic, whenever the underlying model changes. As Mobiscroll components have custom Knockout bindings, they seamlessly integrate in the MVVM pattern. You will notice that no UI modifications are made in our Javascript code.

We will go through the steps of the creation of the app, first building the parts of the UI, than creating the model with the functionality.

Here is what we’re going to discuss:

The User Interface

The Data Structure and Model

The User Interface

The Calendar

Calendar

The calendar is a good choice to get an overview of our to-dos. You can easily navigate with swipe gestures between months and see the days when you have something to do.

To highlight the days we will use the marked property of the calendar, this will indicate that we have a to-do list for that day. Tapping on a marked day will bring up the detailed view of the to-do list. Let’s use the onDayChange event to accomplish that. More on the onDaySelect event handler later.

This is how the calendar initialization looks using the custom Knockout bindings:

1
2
3
4
5
6
7
8
9
10
<div id="calendar" data-bind="
    mobiscroll.calendar: selectedDate,
    mobiscroll.onDayChange: onDaySelect,
    mobiscroll.options: {
        theme: 'ios7-red',
        display: 'inline',
        layout: 'liquid',
        marked: markedDays
        }
"></div>

Simple enough.
You will notice that our calendar looks slightly different than the iOS7 theme. We use a customized version of if built with our Theme Builder called ios7-red.

The Listview

Listview

How to display a to-do list properly? Using a list of course. But we also want to be able to modify our list (add/remove/check/uncheck).
Here is where the Mobiscroll Listview comes in picture. It will enhance our basic html list with gestures and allow us to bind certain actions to them.
In our app we will do the following:

  • On right swipe we check our task and move it to the end of the list marking it as done.
  • On left swipe we remove the task from the list. If it is checked, it will be removed immediately, otherwise needs confirmation.
  • On tap we uncheck the item, but only if it is checked.
  • We are also able to sort our tasks

To define the left and right swipe actions, we will use the stages property. To define the tap action, we will use the onItemTap event.

The Listview Knockout binding it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<ul data-bind="
    mobiscroll.listview: currentList,
    mobiscroll.options: {
        theme: 'ios7',
        sort: true,
        iconSlide: true,
        stages: [
            { percent: 20, icon: 'checkmark', color: '#00CC00', action: checkToDo },
            { percent: -20, icon: 'close', color: '#e61300', action: removeToDo, confirm: confirmDelete }
        ]
    },
    mobiscroll.event: {
        onStageChange: stageChange,
        onItemTap: unCheckToDo,
        onSlideEnd: moveDown,
        onSortUpdate: sortUpdate,
        onSortStart: sortStart,
        onSortStop: sortStop
    }
">
    <li data-bind="attr: { 'data-id': $data.id }, css: {'todocross': $data.done}">
        <div class="textdiv">
            <div data-bind="text: $data.text"></div>
            <div class="linediv"></div>
        </div>
    </li>
</ul>

The great thing about using the Knockout binding that we don’t need to maintain the markup of our list at all, we will just modify our data in the action handlers and Knockout with the Mobiscroll binding will do all the heavy lifting.

We will also add a fancy little animation: if we check or uncheck a to-do item, a strike through animation will occur. We could easily strike the text with css text-decoration, but it cannot be animated, so we will use a div with a border placed over the text which than will be animated with css.

The Add New Task Form

Add new task

We need to add new tasks. Our form will contain a button to toggle the form’s visibility, a single textbox for entering our text, and a button which adds the new item (submits the form). We will bind to the form’s submit event to add the new item, so it will be called if we tap/click on the add button, or press Enter/Go etc. as well.

To show/hide the form, and change the icons on the buttons we will use dynamic css classes using the css binding of the Knockout framework.

The markup of the form with the bindings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="addcontainer">
    <div class="addform" data-bind="css: { addformv: isAddFormVisible }">
        <form data-bind="submit: addToDo">
            <input class="text" data-bind='
                value: toDoText,
                valueUpdate: "afterkeydown",
                hasFocus: isAddFormVisible' />
            <button class="btn btn-check mbsc-lv-ic mbsc-lv-ic-checkmark" data-bind="
                attr: { title: 'Add' }
            "></button>
        </form>
    </div>
 
    <div class="btn btn-add mbsc-lv-ic" data-bind="
        attr: { title: isAddFormVisible() ? 'Cancel' : 'Add' },
        css: { 'mbsc-lv-ic-plus': !isAddFormVisible(), 'mbsc-lv-ic-close': isAddFormVisible , 'btn-cancel': isAddFormVisible },
        click: toggleAddForm
    "></div>
</div>

Once we have the UI elements, let’s move on and create the underlying model and make our app functional.

The Data Structure and Model

All our actions and properties will be encapsulated in a model.

We will store the user’s to-do in the localStorage. In this demo app we will just serialize everything into one JSON string. This might not be the best solution in a real word application, but this is not the purpose of this demo, we just want a simple way to store the data permanently.

A task will have the following properties:

  • id – This will be auto-generated
  • dueDate – Date
  • text – String
  • done – Boolean
1
2
3
4
5
6
function Todo(dueDate, text, done) {
    this.id = uniqueID++;
    this.dueDate = dueDate;
    this.text = text;
    this.done = ko.observable(done);
}

If the user does not have any data in the storage, we will create a sample list for today and another one for 4 days later. The items will be stored in an object where the key is the timestamp of the day, and the value is an array with the item objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var d1 = new Date(),
    d2 = new Date(d1.getTime() + 1000 * 60 * 60 * 24 * 4),
 
// Set hour information to 0 to have a
// timestamp of the beginning of the day
d1.setHours(0, 0, 0, 0);
d2.setHours(0, 0, 0, 0);
 
todos[d1] = [
    { dueDate: d1, text: "Check Mobiscroll demos", done: true },
    { dueDate: d1, text: "Feed the pets", done: false },
    { dueDate: d1, text: "Send an invitation email to Emily", done: false },
    { dueDate: d1, text: "Watch the new Arrow episode", done: false },
    { dueDate: d1, text: "Don't forget Margaret's birthday", done: true }
];
todos[d2] = [
    { dueDate: d2, text: "Wash the car", done: false },
    { dueDate: d2, text: "Feed the pets", done: false },
    { dueDate: d2, text: "Call dad", done: false },
    { dueDate: d2, text: "Buy flowers", done: false }
];

Next we will load the data from the storage or, if it’s empty, take the sample data, and create our to-do objects (the constructor will generate an id for each to-do item):

1
2
3
4
5
6
7
8
9
10
11
12
13
var tempList = [],
    tempObj = {};
 
todos = localStorage.todos ? JSON.parse(localStorage.todos) : todos;
$.each(todos, function (i, v) {
    $.each(v, function (index, todo) {
        tempList.push(new Todo(new Date(todo.dueDate), todo.text, todo.done));
    });
    self.markedDays.push(new Date(i));
    tempObj[i] = tempList;
    tempList = [];
});
todos = tempObj;

Now that we have our data initialized, let’s take a look on our model’s properties and methods:

Properties

  • markedDays: An array with dates which have to-do items. Passed to the calendar in the marked property.
    self.markedDays = [];
  • selectedDate: The currently selected date on the calendar bound to the calendar.
    self.selectedDate = d1;
  • currentList: An observable array which contains the to-do items of the selected date bound to the listview.
    ko.observableArray(todos[self.selectedDate]);
  • toDoText: The text of the new to-do item.
    self.toDoText = ko.observable('');
  • isAddFormVisible: An observable boolean which indicates if the add new form should be displayed or not.
    self.isAddFormVisible = ko.observable(false);
  • isListEmpty: A computed boolean which indicates if list for the selected day is empty or not.
    self.isListEmpty = ko.computed(function () {
        return !self.currentList() || !self.currentList().length;
    });

Methods

  • onDaySelect: Gets called when a day is tapped on the calendar. Populates the currentList with the to-do items of the day. Also hides the add new form, if it was visible. The changeDay flag is used to track if day change is allowed. E.g. during sort or list swipe this is set to false. This only has relevance on devices where multiple touches are possible.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    self.onDaySelect = function (d, inst) {
        if (changeDay) {
            self.selectedDate = d.date;
            self.currentList(todos[self.selectedDate]);
            self.isAddFormVisible(false);
        } else {
            return false;
        }
    };
  • addToDo: Adds a new to-do item to the currently selected day. Only creates the item if text has been entered. If the currently selected day has no items yet, the list is created, the date is added to the list of marked days, and the calendar is refreshed. (Currently Mobiscroll does not support binding directly to the marked setting. If it would be implemented, refresh would be called automatically).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    self.addToDo = function () {
        if (self.toDoText()) {
            if (!todos[self.selectedDate]) {
                todos[self.selectedDate] = [];
                self.currentList(todos[self.selectedDate]);
                self.markedDays.push(self.selectedDate);
                calendar.mobiscroll('refresh');
            }
     
            self.currentList.unshift(new Todo(self.selectedDate, self.toDoText(), false));
            self.isAddFormVisible(false);
            self.save();
        }
        return false;
    };
  • removeToDo: Removes a to-do item from the list. If the list becomes empty, we find remove the currently selected date from the list of the marked days and refresh the calendar.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    self.removeToDo = function (li, item, index) {
        self.currentList.splice(index, 1);
        if (self.currentList().length == 0) {
            $.each(self.markedDays, function (i, d) {
                if (d.getTime() == self.selectedDate.getTime()) {
                    self.markedDays.splice(i, 1);
                    return;
                }
            });
            delete todos[self.selectedDate];
            calendar.mobiscroll('refresh');
        }
        self.save();
        return false;
    };
  • stageChange: This will be called during the swipe of a list item, whenever the stage changes. Once we passed our “Check” stage (20%), and the item is not already checked, we set the corresponding to-do item as done immediately (we don’t wait until the swipe is finished), and rememeber it in the changeOnStage private variable. If the item leaves the “Check” stage, we set the done property back to false, but only if it was changed during this swipe.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    self.stageChange = function (item, index, currStage) {
        changeDay = false;
        if (currStage.percent == 20 && !self.currentList()[index].done()) {
            changeOnStage = true;
            self.currentList()[index].done(true);
        } else if (currStage.percent == 0 && changeOnStage) {
            self.currentList()[index].done(false);
        }
    };
  • checkToDo: Runs when the list item is released on the “Check” stage. Sets the item as done and saves data to the storage. If the item was checked and it’s not already at the and of the list, we will move it to the end of the list in our moveDown method, so we will return false, to prevent the item to slide back in its initial position.

    1
    2
    3
    4
    5
    6
    7
    8
    
    self.checkToDo = function (li, item, index) {
        if (changeOnStage || !self.currentList()[index].done()) {
            self.currentList()[index].done(true);
            self.save();
            changeOnStage = true;
            return self.currentList().length - 1 == index;
        }
    };
  • unCheckToDo: Sets the item as undone. Gets called when a list item is tapped.

    1
    2
    3
    4
    
    self.unCheckToDo = function (item, index) {
        self.currentList()[index].done(false);
        self.save();
    };
  • moveDown: Moves the item to the end of the list using the move method of the listview. Gets called at the end of the swipe gestures. The item will be moved only if the state was modified during the swipe and it’s not already at the end of the list. It also resets the changeDay and changeOnStage flags.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    self.moveDown = function (item, index) {
        if (changeOnStage && self.currentList().length - 1 != index && self.currentList()[index].done()) {
            $(this).mobiscroll('move', item, self.currentList().length - 1, null, function () {
                self.sortUpdate.call(this, item, index);
            });
        }
        changeDay = true;
        changeOnStage = false;
    };
  • sortStart: Set the changeDay flag when a sort is started.

    1
    2
    3
    
    self.sortStart = function (item, index, inst) {
        changeDay = false;
    };
  • sortStop: Unset the changeDay flag when a sort is finished.

    1
    2
    3
    
    self.sortStop = function (item, index, inst) {
        changeDay = true;
    };
  • sortUpdate: Sorts the array of the to-do items based on their position on the UI. Gets called when a sort occurred on the UI or an item was moved at the end of the list. Saves the changed data.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    self.sortUpdate = function (item, index, inst) {
        var list = this;
        self.currentList.sort(function (left, right) {
            var i1 = $('li', list).index($('li[data-id="' + left.id + '"]', list)),
                i2 = $('li', list).index($('li[data-id="' + right.id + '"]', list));
     
            return i1 - i2;
        });
        self.save();
    };
  • confirmDelete: Returns true if the delete action needs to be confirmed (the to-do item is not yet done).

    1
    2
    3
    
    self.confirmDelete = function (li, index, item) {
        return !self.currentList()[index].done();
    };
  • toggleAddForm: Toggles the visibility of the add form. Bound to the click event of the “+” button.

    1
    2
    3
    4
    
    self.toggleAddForm = function () {
        self.isAddFormVisible(!self.isAddFormVisible());
        self.toDoText('');
    };
  • save: Saves the data to the localStroge. Id’s are not stored.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    self.save = function () {
        if (hasStorage) {
            var todo = [], a = {};
     
            $.each(todos, function (i, v) {
                $.each(v, function (index, t) {
                    todo.push({ text: t.text, dueDate: t.dueDate, done: t.done() });
                });
                a[i] = todo;
                todo = [];
            });
            localStorage.todos = JSON.stringify(a);
        }
    };


All of the above could be written of course without Knockout and MVVM, but we would need to do all the UI updates, event bindings… ourselves. And of course scaling is easier this way as well.

Downloads

Go ahead and download the app source and see how it looks all together. You will need to include your Mobiscroll build separately.

Download the App Code


Did we leave something out, do you have anything to add, comment? Please let us know in the comment section below!

Start building better products
Sign up and get posts like this in your inbox. We respect your time and will send you only the good stuff!

Tags: , , , , , , , ,