Click here to Skip to main content
Click here to Skip to main content

TaskTracker Offline Web Application for Mobile using HTML5/jQuery/KnockoutJS

, 30 Dec 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
Task Tracker is an offline web application developed using HTML5, CSS3, and JavaScript.

TaskTracker

Introduction

You are watching an interesting YouTube video on your iPhone while commuting to work in a train and the network gets disrupted and you can't watch the video. You get frustrated. This is the limitation of typical web based applications as they need internet connection to serve the application seamlessly. But with HTML5, it is now possible to develop off-line web applications which will continue to work seamlessly in the event of loss of internet connection. This is possible because of the Application Cache and Web Storage features of HTML5. With Web Storage, now we can store up to 5 MB of data on the client side. This is quite a decent size and will enable us to cache data on the client side in the event of internet loss and the user will be able to continue working and then sync with the server once internet connection is restored. The purpose of this article is to demonstrate these features. TaskTracker is an offline web application targeted for the mobile platform and will help users to keep track of their pending tasks. Also, they can maintain and manage their contacts. I will also explain how the power of HTML5 and open source frameworks such as jQuery/jQueryUI/jQuery-Validation and Knockoutjs are used to build this application.

Application Cache and Web Storage features of HTML5 are used in this application.

With jQuery we can do easy manipulation of DOM elements such as attaching an event, adding/removing or toggling CSS classes dynamically with simple and concise code, and above all it is cross browser compatible! So we can focus on the business logic of our application without worrying about these issues.

The jQuery-UI plugin provides the power to convert standard HTML markup elements into elegant GUI Widgets with only a few lines of code. Also, it provides a variety of Themes, so we don't have to worry too much about building our own CSS styles.

The jQuery-Validation plugin provides all the features to take care of client side validations.

Finally, Knockoutjs is a wonderful framework which provides us the capability to build applications using the Model-View-ViewModel (MVVM) design pattern. This is a very powerful design pattern and is very popular in applications developed using WPF and Silverlight. Knockoutjs provides a powerful data binding framework to be used when programming using JavaScript. There is an excellent tutorial available to get familiar with this framework.

Background

A couple of months back I watched an interesting video exploring the power of HTML5 and was really impressed by its powerful features. The features of HTML5, together with the power of CSS3 and JavaScript, can be used to build web applications for the mobile platform and then can be converted to native mobile applications for different mobile platforms using an Open Source framework such as PhoneGap. So we can write once but deploy it on multiple platforms. I decided to develop a TaskTracker application to explore these features. I will explain the various features of the different frameworks used in this application in the below sections. You can view a working demo here.

Design Overview

The application is built using the popular MVVM design pattern. MVVM is a design pattern very similar to the old MVC design pattern and addresses the separation of concerns. Like MVC, it has a model, view, but the controller role is done by the ViewModel. View Model is responsible for handling user interactions and also interacts with the model and updates the view. It can have additional attributes apart from the Model which can be View specific. Also, in a complex web application comprising of multiple views, each view can have a corresponding view model. In our application there is only one view and one view model. The key files of the application are main.html, taskcontroller.js, and storageController.js. The class diagram of the application is shown in Figure 1.

Class diagram of Task Tracker

From Figure 1, the TaskControllerUI class is a conceptual class and represents the UI for the application and is implemented as plain HTML. TaskViewModel is a key class responsible for handling all the user interactions and updating the view and interacts with the model. Customer and Task are two entity classes which act as a model for this application. StorageController is a wrapper class and is responsible for saving and retrieving data from WebStorage.

Since JavaScript is not like conventional object oriented programming languages such as C++, C#, or Java, it does not have a Class construct. So all the classes are implemented differently. I will first explain the business layer implementation, then the data layer, and finally UI.

Business Layer

The Business layer consists of two key entities: Task and Customer. Another important class is a TaskViewModel. The below section shows the implementation details of Task and Customer.

// Declare Task class
 function Task() {
    var self = this;
    self.name= ko.observable("");
    self.id = 1,
        self.description = ko.observable(""),
        self.startDate = ko.observable($.datepicker.formatDate('yy-mm-dd',new Date())),
        self.visitDate = ko.observable($.datepicker.formatDate('yy-mm-dd',new Date())),
        self.visitTime = ko.observable("9:00 am"),
        self.price = ko.observable(0),
        self.status = ko.observable("Pending"),
        self.custid = ko.observable();
    self.notes = ko.observable("");

}
// Declare Customer Class
function Customer() {
    var self = this;
    self.custid = ko.observable(101);
    self.firstname = ko.observable("");
    self.lastname = ko.observable("");
    self.address1 = ko.observable("");
    self.address2 = ko.observable("");
    self.city = ko.observable("");
    self.country = ko.observable("");
    self.zip = ko.observable("");
    self.phone = ko.observable("");
    self.mobile = ko.observable("");
    self.email = ko.observable("");


}

// example of creating new instance of these classes
var task = new Task();
var cust = new Customer();
// Important point to remember knockout observables are methods.
var fname= cust.firstname(); // see we access the property as function.
// set the property.
cust.firstname('Joe'); // setting the observable property.

If you look at the above code you might have noticed that both the classes contain only properties as they are entity classes and do not expose any methods. But also you might have noticed that these properties are initialized with Knockout constructs. These properties are declared as observables. This will enable these properties bind to the UI and will provide two-way binding. So any changes done to these properties in UI will automatically update the properties. Or if you change their values programmatically, then it will reflect those changes in the UI. One thing to remember is that Knockout observables are functions and you can't access or initialise them as normal properties. See the above code showing an example of setting and accessing a Knockout observable property.

Let us see now the implementation details of TaskViewModel.

function TaskViewModel() {
    var self =this;
    self.lastID = 1000;
    self.custID = 100;
    self.taskCustomer = ko.observable();
    self.currentCustomer = ko.observable(new Customer());
    self.customerSelected = -1;
    self.customers = ko.observable([]);
    self.normalCustomers = [];
    self.selected = ko.observable();
    self.taskMenu = ko.observable("Add Task");
    self.customerMenu = ko.observable("Add Customer");
    self.editFlag = false;
    self.editCustFlag = false;
    self.tasks = ko.observableArray([]);
    self.normalTasks = [];
    self.currentTask = ko.observable(new Task());
    self.taskOptions = ['Pending', 'In Progress', 'Complete'],
    self.filterCustomer = ko.observable("");
   
    self.filterDate = ko.observable("");
    //Filter tasks by visit date
    self.ftasks = ko.dependentObservable(function () {
        var filter = self.filterDate();
        if (!filter) {
            return self.tasks();

        }
        return ko.utils.arrayFilter(self.tasks(), function (item) {
            if ($.isFunction(item.visitDate)) {
                var val1 = item.visitDate();
                var val2 = self.filterDate();
                if (item.status() === 'Complete')
                    return false;
                return (val1 === val2);
            } else {
                return self.tasks();
            }
        });

    }, self);

    // Methods for managing the tasks.

    // Init tasks
    self.init = function () {

        var sLastID = tsk_ns.storage.getItem("lastTaskID");
        if (sLastID !== null) {
            self.lastID = parseInt(sLastID);
        }
        // Read tasks.
        var sTasks = tsk_ns.storage.getItem("tasks");
        //  alert(sTasks);
        if (sTasks !== null) {

            self.normalTasks = JSON.parse(sTasks);
            self.updateTaskArray();
            self.currentTask(new Task());
            // this.tasks(ko.observableArray(ntasks));
            //this.tasks(ntasks);

        } else {

            alert("No Tasks in storage");
        }

    },
    // Add task
    self.addTask = function () {
        $('#taskForm').validate().form();
        var isvalid = $('#taskForm').valid();
        if (isvalid) {
            if (!self.editFlag) {
                self.lastID += 1;
                self.currentTask().id = self.lastID;
                self.tasks.push(self.currentTask);
                tsk_ns.storage.saveItem("lastTaskID", self.lastID.toString());

            } else {
                self.currentTask().custid = self.taskCustomer().custid;

                //self.selected(self.currentTask);
            }
            // Save last task ID
            //self.tasks(self.normalTasks);

            // save tasks
            var mTasks = ko.toJSON(self.tasks);
            console.log(mTasks);
            tsk_ns.storage.saveItem("tasks", mTasks);
            self.normalTasks = JSON.parse(mTasks);
            self.updateTaskArray();

            // current taks
            self.taskMenu("Add Task");
            self.currentTask(new Task());
            self.editFlag = false;
            console.log("No of tasks are :" + self.tasks().length);
        }
    };

    // Remove tasks.
    self.removeTask = function (itask) {

        self.tasks.remove(itask);
        var mTasks = ko.toJSON(self.tasks);
        self.normalTasks = JSON.parse(mTasks);
        tsk_ns.storage.saveItem("tasks", mTasks);
        self.updateTaskArray();
    };

    // Edit task
    self.editTask = function (itask) {
        self.selected(itask);
        //    var index=ko.utils.arrayIndexOf(self.tasks, self.selected);
        // Get Current customer

        var curCust = self.getCurrentCustomer(itask.custid);
        if (curCust !== null) {
            self.taskCustomer(curCust);
        }

        self.currentTask(itask);
        self.taskMenu("Edit Task");
        $("#tabs").tabs("option", "selected", 2);
        self.editFlag = true;
        //$("#taskNew").button("option", "disabled", true);
        //$("#taskUpdate").button("option", "disabled", false);

    };

    self.addNotes = function (itask) {

        var curCust = self.getCurrentCustomer(itask.custid);
        if (curCust !== null) {
            self.taskCustomer(curCust);
        }

        self.currentTask(itask);
        $("#dlgNotes").dialog("open");
        self.editFlag = true;

    };

    // Update task
    self.updateTask = function () {
        console.log("Select task index is " + this.selected);
        console.log(self.taskCustomer().custid);
        // self.currentTask().custid = self.taskCustomer.custid;
        var normalTsk = ko.toJSON(self.currentTask());
        // self.currentTask().visitDate(normalTsk.visitDate);
        var tskObject = JSON.parse(normalTsk);
       // var stdate = $("#taskStartDate").datepicker(
        tskObject.custid = self.taskCustomer().custid;
        console.log(tskObject.custid);
        if (this.selected > -1)
            this.normalTasks[this.selected] = tskObject;
    };

    self.getCurrentCustomer = function (cid) {
        for (var i = 0; i < self.normalCustomers.length; i++) {
            var c1 = self.normalCustomers[i];
            if (c1.custid === cid)
                return c1;
        }
        return null;

    }

    // copy tasks.
    self.updateTaskArray = function () {

        self.tasks.removeAll();
        // self.tasks(self.normalTasks);
        for (var i = 0; i < self.normalTasks.length; i++) {
            var ctask = self.normalTasks[i];
            // var ctask = JSON.parse(self.normalTasks[i]);

            var t = new Task();
            t.id = ctask.id;
            t.name(ctask.name);
            t.description(ctask.description);
            t.startDate(ctask.startDate);
            t.visitDate(ctask.visitDate);
            t.visitTime(ctask.visitTime);
            t.price(ctask.price);
            t.status(ctask.status);
            t.custid = ctask.custid;
            t.notes(ctask.notes);
            self.tasks.push(t);
            // console.log("Task name is " + ctask.name);
        };
        console.log("No of tasks are :" + self.tasks().length);
    };

    self.toggleScroll= function() {
        
        $("#taskcontent").toggleClass("scrollingon");
    };
}

TaskViewModel is the core class of this application and is responsible for handling user interactions and interacts with the model and updates the view. The above code is not the complete source code, but just a highlight of the key methods. The view model has two observable arrays for Tasks and Customers. Also note the use of dependent observable to filter the pending tasks by visit date. Please refer to the source code for the full implementation details.

Data Layer

The data layer is implemented by the StorageController class. The details of its implementation are given below:

// Store Manager Singleton class
// Implemented using revealing module design pattern
var tsk_ns = tsk_ns || {};

tsk_ns.storage = function () {
    // create instance of local storage
    var local = window.localStorage,
    // Save to local storage
        saveItem = function (key, item) {
            local.setItem(key, item);
        },
        getItem = function (key) {
            return local.getItem(key);
        },
        hasLocalStorage = function () {

            return ('localStorage' in window && window['localStorage'] != null);
        };

    // public members
    return {
        saveItem: saveItem,
        getItem: getItem,
        hasLocalStorage: hasLocalStorage

    };
} ();

The above class is just a wrapper to window.localStorage and provides a wrapper method to save and get data from the local storage. The class has a hasLocalStorage method which checks whether the browser supports localStorage.

UI Layer

The UI layer is implemented as a plain HTML file.

Once all the documents are loaded it executes the below initialization code. The code defines the validation rules, jQueryUI initialization code, and binding of the view model to the UI.

// Document ready
$(function () {
    // Define Validation rules for customer form
    $('#customerForm').validate({
        rules: {
            firstname: "required",
            lastname: "required",
            //compound rule
            email: {
                email: true

            }
        }
    });

    // Define validation options for taskform
    $('#taskForm').validate({
        rules: {
            taskname: "required",
            startdate: {
                dateITA: true

            },
            visitdate: {

                dateITA: true
            },
            visittime: {
                time12h: true
            },

            price: {
                number: true,
                min: 0
            }

        },
        messages: {
            startdate: {
                dateITA: "Invalid Date! Enter date in (dd/mm/yyyyy) format."
            },
            visitdate:{
                dateITA: "Invalid Date! Enter date in (dd/mm/yyyyy) format."
            },
        }
    });
    // Initialise GUI widgets.
    $("#tabs").tabs();
    $("#taskNew,#taskSave,#taskUpdate").button();
    $("#taskNew").button("option", "disabled", false);
    $("#taskUpdate").button("option", "disabled", true);
    //Create instance of view model and bind to UI.
    var viewModel = new TaskViewModel();
    viewModel.init();
    viewModel.initCustomers();
    ko.applyBindings(viewModel);

    // Initialise Dialog.
    $("#dlgNotes").dialog({
        autoOpen: false,
        modal: true,
        buttons: {
            Ok: function () {

                $(this).dialog("close");
                viewModel.addTask();
            }
        }
    });
});

I have used different types of bindings such as for-each, with, form, and custom binding features of KnockoutJS. Please refer to the source code for complete details. Below is the code shown for the Customer custom binding.

ko.bindingHandlers.customerFromID = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var options = allBindingsAccessor().viewmodelOptions || {};
        var custid = valueAccessor();
        var vm = options.ViewModel;
        var sfield = options.showField;
        var curCust = vm.getCurrentCustomer(custid);
        if (curCust != null) {
           if(sfield===1)
             $(element).text(curCust.firstname + '_' + curCust.lastname);
           else if(sfield===2)
               $(element).text(curCust.phone);
           else if(sfield==3)
               $(element).text(curCust.mobile);
           
        }
        else
            $(element).text('');
    }
}

In pending task list table I wanted to show the details of Customer such as customer name, phone, and mobile. Since each row is bound to a Task object which has the customerID as one of its properties, I had to use the custom binding feature of KnockoutJS to extract customer details from the customer ID. Please refer to the above code for details.

Configuring for Offline Working

With HTML5 we can now configure the pages, assets, and resources of the application which should be served offline. This is possible by defining the Application Manifest file, which is a plain text file. Also, in the html tag, you need to define the manifest attribute to specify a name for the manifest file. <html manifest="manfiest.appcache"> I have added the manifest file with an extension of .appcache. But you can use any extension. We need to configure the web server to include this new extension type as well as specify its MIME type as text/cache-manifest. Please refer to the code below to see the content of the manifest file.

Please remember that the fist line in the manifest file should be CACHE MANIFEST. Don't put any comments before this line. The structure of the manifest file is simple. It has three sections CACHE, NETWORK, and FALLBACK. All the pages and resources used off-line need to be specified under the CACHE section. Pages/resources required only online are under the NETWORK section. In the FALLBACK section you can specify alternate pages in case of fallback. That's it!

The browser supporting the application cache will initially save all the offline resources on the client side and then it will always serve these resources from the application cache irrespective of whether you are online or offline. So any changes done to these pages will reflect only when the manifest file is modified. This is an important point to remember. Modify the manifest file and update the version to indicate to the browser that there are some changes so that it will bring the latest changes from the server and save it again on the client side.

//contentmanifest.appcache
CACHE MANIFEST
## version 1.6

CACHE:
themes/le-frog/jquery-ui.css
themes/le-frog/jquery-ui-1.8.17.custom.css
css/tasktracker.css
scripts/jquery-1.7.1.min.js
scripts/json2.js
scripts/jquery-ui-1.8.17.custom.min.js
scripts/jquery-ui-timepicker-addon.js
scripts/knockout-latest.js
scripts/ko-protected-observable.js
scripts/StorageManager.js
scripts/TaskController.js
scripts/jquery.validate.min.js
scripts/additional-methods.js
images/app-note-icon.png
images/Actions-document-edit-icon.png
images/edit-trash-icon.png

NETWORK:
# Other pages and resources only to be served online will come here
FALLBACK:
# Any alternate pages in case of fallback will come here.

Styling for Mobile

I don't have to do major changes to configure this application for mobile. The only thing you need to specify is the viewport setting in the head section of your HTML file. I had to define an extra style for the textarea tag as its width was overflowing outside its parent container. Please refer to the code below for details.

// <meta name="viewport" content="width=device-width, initial-scale=1">
// styling for textarea
.fixwidth
{
    width:80%;    
}

Points of Interest

Initially I declared the startdate and visitdate properties of the Task class as Date type. But I faced issues while saving and retrieving data. Incorrect date was showing in the DatePicker control. So I modified the code to save it as a string.

Since the application does not have any server side processing, client side validation is triggered using the below code before saving Task or Customer details.

// Validation for Task Entry form
$('#taskForm').validate().form();
var isvalid = $('#taskForm').valid();
// Validation for customer entry form.
$("#customerForm").validate().form();
var custValid=  $("#customerForm").valid();

Also on iPhone or Android phones, because of screen width limitations, I couldn't display all the columns in the pending task list table as it was overflowing its container. I tried to use a CSS style to show the scroll bar. It works fine in a Desktop browser but not on a SmartPhone. So I used the below code on the click event of the tr tag of the pending task list table. The user can tap on the heading to collapse or expand the row. This is achieved using the toggleClass method of jQuery.

// toggleScroll method of TaskController class
self.toggleScroll= function() {

    $("#taskcontent").toggleClass("scrollingon");
};

Android Phone/iPhone/iPad Users

Android users can download the native app from here. iPhone and iPad users can access the application View Demo here. They can bookmark this page. It will then work always whether you are online or offline.

History

This is the first version of TaskTracker. In a future version, I am thinking of saving tasks and customer details on a server side database. So the application can sync with the database when connected to the internet. This way all the data will be accessible from anywhere, either from a mobile or a desktop browser. Which is not the case with this version as data is stored on the client browser.

Conclusion

Due to the new features in HTML5 it is now easy to develop off-line web applications. The application explores the key features of HTML5, jQuery, jQuery-UI, jQuery-Validation, and KnockoutJs. Also we can use an Open Source framework such as PhoneGap to convert it to a native application for different mobile platforms. Please let me know your comments and suggestions. You can e-mail me for any queries or clarifications about the implementation.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

gokuldas
Software Developer (Senior)
United Kingdom United Kingdom
I am Solution Architect with 20+ years of IT experience in the field of real time,embedded,client/server and web based applications and Business Intelligence . I am currently working as Senior Consultant for Infor.

Comments and Discussions

 
QuestionAndroid not found ? Pinmemberdeloteric19-Dec-13 22:29 
AnswerRe: Android not found ? Pinmembergokuldas20-Dec-13 0:06 
GeneralRe: Android not found ? Pinmemberdeloteric20-Dec-13 0:30 
Questionthanks for the tasktracker PinmemberDenno.Secqtinstien5-Feb-13 19:58 
QuestionVisual Studio Solution File PinmemberSheamus21-Sep-12 12:52 
QuestionWonderful article! PinmemberSuper Tango25-Jul-12 9:07 
AnswerRe: Wonderful article! Pinmembergokuldas25-Jul-12 9:47 
GeneralRe: Wonderful article! PinmemberSuper Tango25-Jul-12 12:32 
GeneralRe: Wonderful article! Pinmembergokuldas26-Jul-12 3:43 
QuestionAre you any close to your second article? PinmemberWagid Sheikh8-Jul-12 21:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141223.1 | Last Updated 30 Dec 2013
Article Copyright 2012 by gokuldas
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid