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.
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
.
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 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("");
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 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:
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
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.
$(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
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.
.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.
$('#taskForm').validate().form();
var isvalid = $('#taskForm').valid();
$("#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.
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.