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.

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.
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("");
}
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("");
}
var task = new Task();
var cust = new Customer();
var fname= cust.firstname(); cust.firstname('Joe');
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("");
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);
self.init = function () {
var sLastID = tsk_ns.storage.getItem("lastTaskID");
if (sLastID !== null) {
self.lastID = parseInt(sLastID);
}
var sTasks = tsk_ns.storage.getItem("tasks");
if (sTasks !== null) {
self.normalTasks = JSON.parse(sTasks);
self.updateTaskArray();
self.currentTask(new Task());
} else {
alert("No Tasks in storage");
}
},
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;
}
var mTasks = ko.toJSON(self.tasks);
console.log(mTasks);
tsk_ns.storage.saveItem("tasks", mTasks);
self.normalTasks = JSON.parse(mTasks);
self.updateTaskArray();
self.taskMenu("Add Task");
self.currentTask(new Task());
self.editFlag = false;
console.log("No of tasks are :" + self.tasks().length);
}
};
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();
};
self.editTask = function (itask) {
self.selected(itask);
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;
};
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;
};
self.updateTask = function () {
console.log("Select task index is " + this.selected);
console.log(self.taskCustomer().custid);
var normalTsk = ko.toJSON(self.currentTask());
var tskObject = JSON.parse(normalTsk);
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;
}
self.updateTaskArray = function () {
self.tasks.removeAll();
for (var i = 0; i < self.normalTasks.length; i++) {
var ctask = 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("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
var tsk_ns = tsk_ns || {};
tsk_ns.storage = function () {
var local = window.localStorage,
saveItem = function (key, item) {
local.setItem(key, item);
},
getItem = function (key) {
return local.getItem(key);
},
hasLocalStorage = function () {
return ('localStorage' in window && window['localStorage'] != null);
};
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.
$(function () {
$('#customerForm').validate({
rules: {
firstname: "required",
lastname: "required",
email: {
email: true
}
}
});
$('#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."
},
}
});
$("#tabs").tabs();
$("#taskNew,#taskSave,#taskUpdate").button();
$("#taskNew").button("option", "disabled", false);
$("#taskUpdate").button("option", "disabled", true);
var viewModel = new TaskViewModel();
viewModel.init();
viewModel.initCustomers();
ko.applyBindings(viewModel);
$("#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.
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.
.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.
$('#taskForm').validate().form();
var isvalid = $('#taskForm').valid();
$("#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.
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.