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

TaskTracker Offline Web Application for Mobile using HTML5/JQUERY/KNOCKOUTJS

By , 26 Feb 2012
 
TaskTracker

Introduction

You are watching an interesting Youtube video on your iPhone while commuting to work in a Train and network gets disrupted and you can't watch the video. You get frustrated. This is the limitation of typical web based application as they need internet connection to serve the application seamlessly.But with HTML5 it is now possible to develop off-line web application which will continue to work seamlessly in the event of loss of internet connection. This is possible because of Application Cache and Webstorage features of HTML5. With webstorage now we can store upto 5 MB of data on client side. This is quite a decent size and will enable us to cache data on client side in the event of internet loss and user will be able to continue working and then sync with the server once internet connection is restored. Purpose of this article is to demonstrate these features. TaskTracker is an offline web application targeted for 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/Jquery-UI/Jquery-Validation and Knockoutjs are used to build this application.

Application Cache and Webstorage 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 toggle CSS classes dynamically with simple and concise code and above all it is cross browser compatible!. So we can focus on business logic of our application without worrying about these issues.

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

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 the application using Model-View-ViewModel (MVVM) design pattern. This is very powerful design pattern and is very popular in applications developed using WPF and Silverlight. Knockoutjs provides powerful data binding framework to be used when programming using javascript.There is an excellent tutorial available to get familiar with this framework.

Background

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

Design Overview

The application is built using popular MVVM design pattern. MVVM is a design pattern very similar to old MVC design pattern and addresses the separation of concerns. Like MVC it has model,view,but controller role is done by ViewModel. View Model is responsible for handling user interactions and also interacts with model and updates the view. It can have additional attributes apart from Model which can be view specific. Also in a complex web application comprising of multiple views,each view can have 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 the figure 1, TaskControllerUI class is a conceptual class and represents the UI for the application and is implemented as plain html. TaskViewModel is key class responsible for handling all the user interactions and updating the view and interacts with model. Customer and Task are two entity classes which acts as a model for this application.StorageController is a wrapper class and is responsible for saving and retrieving data from WebStorage.

Since Java script is not like conventional object oriented programming languages such as C++, C# or Java and it does not have 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

Business layer consists of two key entities Task and Customer. Another important class is a TaskViewModel 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 see at the above code you might have noticed that both the classes contains only properties as they are entity classes and do not expose any methods. But also you might have noticed that these properties are initialised with knockout constructs.These properties are declared as observables.This will enable these properties bind to 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 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 example of setting and accessing 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 core class of this application and is responsible for handling user interactions and interacts with model and updates the view. The above code is not complete source code,but just highlight 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 the source code for full implementation details.

Data Layer

Data layer is implemented by StorageController class. Details of it's 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 wrapper method to save and get the data from the local storage. The class has hasLocalStorage method which checks whether browser supports the localStorage.

UI Layer

UI Layer is implemented as plain html file.

Once all the document is loaded it excutes below initialisation code.The code defines validation rules, JQuery-UI initialisation code and binding of view model to 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 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 Task object which has customerID as one of its property. I had to use the custom binding feature of Knockoutjs to extract customer details from customer ID. Please refer 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 plain text file. Also in html tag you need to define the manifest attribute to specify name of the manifest file. <html manifest="manfiest.appcache"> I have added manifest file with the extension of .appcache. But you can use any extension. But we need to configure the webserver to include this new extension type as well as specify its MIME type as text/cache-manifest.Please refer code below to refer the content of the manifest file.

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

Browser supporting the application cache will initially save all the offline resources on client side and then it will always serve these resources from application cache irrespective of whether you are online or offline. So if any changes done to these pages will reflect only when manifest file is modified. This is important point to remember. Modify the manifest file and update the version to indicate to the browser that there is some change so that it will bring latest changes from the server and save it again on 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 change to configure this application for mobile. Only thing you need to specify is viewport setting in the head section of your html file. I had to define extra style for textarea tag. As its width was overflowing outside its parent container. Please refer 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 startdate and visitdate properties of Task class as Date type. But I faced issues while saving and retrieval of data. Incorrect date was showing in the datepicker control. So I modified the code to save it as string.

Since the application does not have any server side processing.Client side validation is triggered using 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 phone because of screen width,I couldn't display all the columns in pending task list table as it was overflowing it's container. I tried to use CSS style to show the scroll bar. It works fine in Desktop browser but not on smart phone. So I used below code on click event of tr tag of pending task list table. User can tap on heading to collapse or expand the row. This is achieved toggleClass method of Jquery.

// toggleScroll method of TaskController class
        self.toggleScroll= function() {
        
            $("#taskcontent").toggleClass("scrollingon");
        };

Android Phone/iPhone/iPad Users

I have published this application as native application on Android market and is available for free download.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 future version,I am thinking of saving tasks and customer details on server side database. So the application can sync with the database when connected to internet. This way all the data will be accessible from any where either from mobile or desktop browser.Which is not the case with this version as data is stored on client browser.

Conclusion

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

License

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

About the Author

gokuldas
Software Developer (Senior) Infor
United Kingdom United Kingdom
Member
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 with Infor UK Ltd.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Questionthanks for the tasktrackermemberDenno.Secqtinstien5 Feb '13 - 18:58 
QuestionVisual Studio Solution FilememberSheamus21 Sep '12 - 11:52 
QuestionWonderful article!memberSuper Tango25 Jul '12 - 8:07 
AnswerRe: Wonderful article!membergokuldas25 Jul '12 - 8:47 
GeneralRe: Wonderful article!memberSuper Tango25 Jul '12 - 11:32 
GeneralRe: Wonderful article!membergokuldas26 Jul '12 - 2:43 
QuestionAre you any close to your second article?memberWagid Sheikh8 Jul '12 - 20:14 
AnswerRe: Are you any close to your second article?membergokuldas8 Jul '12 - 22:59 
GeneralRe: Are you any close to your second article?memberWagid Sheikh16 Jul '12 - 19:53 
GeneralRe: Are you any close to your second article?membergokuldas16 Jul '12 - 22:31 
QuestionExcellent article, I'm anxious to your next articlememberMember 92074733 Jul '12 - 21:34 
QuestionExcellent article !! I have a questionmembervishakhakhadse1 Jun '12 - 19:45 
AnswerRe: Excellent article !! I have a questionmembergokuldas3 Jul '12 - 6:02 
QuestionWonderful Task Tracker examplememberHarry Bolzak30 May '12 - 16:47 
AnswerRe: Wonderful Task Tracker examplemembergokuldas3 Jul '12 - 6:04 
GeneralRe: Wonderful Task Tracker examplememberMember 92074733 Jul '12 - 21:31 
GeneralMy vote of 5membermanoj kumar choubey29 Feb '12 - 18:09 
GeneralMy vote of 5memberBryanWilkins27 Feb '12 - 8:06 
GeneralRe: My vote of 5membergokuldas28 Feb '12 - 1:27 
QuestionNicemvpSacha Barber27 Feb '12 - 6:16 
GeneralMy vote of 5memberMember 86818627 Feb '12 - 3:29 
GeneralMy vote of 4memberhjgode26 Feb '12 - 21:05 
QuestionMy 5mvpMehdi Gholam26 Feb '12 - 19:34 
GeneralMy vote of 5memberMahmud Hasan26 Feb '12 - 16:42 

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 26 Feb 2012
Article Copyright 2012 by gokuldas
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid