With Binding Magic! Writing Single Page Application with JQuery Mobile and Knockoutjs
April 10th, 2014 — 15 min read
Greetings! Its nice to have yet another post regarding knockoutjs. Today i am going to define how we can create Single Page Application using JQuery Mobile and Knockoutjs. You may already have seen some posts on this topic so what’s new in here? The only thing is that i am going to implement the technique which i defined here. Please go through this post of mine and you will thoroughly understand how easy it is to make SPA with JQuery Mobile and knockoutjs.This is pretty simple and gives a good control over the application. After we finish these steps the application will be ready for phone gap to get hands on it. So lets get started.
The Application Structure
Before we dig we need to see how we can organize the application. I have created this structure which suits my needs but you can choose any according to your requirements.
/knockoutjs-jqm-spa/ /js/ /jquery.js /knockout-3.0.0.js /jquery.mobile-1.3.2.js /viewmodel.js /models/ /home.js /list.js /form.js /css/ /style.css /jquery.mobile-1.3.2.css /images /images index.html |
OK. Now lets go further to see how we can implement this. First we will see how we can use jquery mobile then we will see how we can use knockoutjs.
The Basic Structure of Page
In the first step we need to see how JQuery mobile is implemented on html. The common approach is using sections and assigning data-role = ‘page’ property so that sections can be displayed as pages. In this application i will be using three pages so we need three sections for three pages. Here is the basic page.
<body> <!-- /////// home page start /////// --> <section id="home" data-role="page"></section> <!-- /////// home page end /////// --> <!-- /////// list page start /////// --> <section id="list" data-role="page"></section> <!-- /////// list page end /////// --> <!-- /////// form page start /////// --> <section id="form" data-role="page"></section> <!-- /////// form page end /////// --> </body> |
Each section has an id. These are used to navigate between pages. How we will be navigating ? Lets see the header which has some links to navigate.
<header> <nav class="v2_n"> <a href="#home" class="active" >Home</a> <a href="#list" data-transition="fade" >List</a> <a href="#form" data-transition="fade" >Form</a> </nav> </header> |
Here each link has href attribute which is linked to specific section on the page. Other important thing is transition. You can use any transition like fade , pop and slide.
Parent Model
Now as the basic page is ready we can see the kncokout implementation. Here is the knockout model we will be using. I call it parent model as this will be serving as parent model.
function VM() { var self = this; self.Title = ko.observable('With Binding Magic! SPA with Knockout and JQM!') self.HomePage = ko.observable() self.FormPage = ko.observable() self.ListPage = ko.observable() self.Data = ko.observableArray([]) self.LastViewedPage = ko.observable('Home') self.LastPageVisited = ko.observable() self.LastPage = function(){ self.LastViewedPage(self.LastPageVisited()) self.HomePage().ModifyMessage(true) return true } } |
Let me define this model. I have defined an observable property `Title`. This will be used to display application title. Making it in parent model is due to write succinct and DRY code so that we can avoid code repetition. For three sections(pages) i have created three observable properties in the parent model. Each of which will be having a child model i will define later. Other properties `Data`, `LastViewedPage` and `LastVisitedPage` are only to define how we can use parent properties throughout the application. These properties can be served to hold data. In other words, these are alternate of form submit data. When a form is submit you can get its data on the second page. Or you can save data in session so that it is vailable for the other pages. These properties serve the same thing. Here you see a method `LastPage` which i will define later.
You can create as many observable properties to serve for pages as you need and as many other properties as you need to use among pages. Save this file as `viewmodel.js`in the js folder.
Sub Models (Child Models)
Now as we have created an observable property for home page as Home so we need a child model for this. Here is the child model for Home observable of parent model.
home.js
var homePage = function(parent) { var self = this self.Parent = ko.observable(parent) self.GreetingMessage = ko.observable('Hi! I am home page') self.ModifyMessage = ko.observable(false) self.LoadData = function(){ if(self.ModifyMessage()){ var message = 'You have just landed here from '+self.Parent().LastPageVisited() self.GreetingMessage(message) self.ModifyMessage(false) } } } |
In this model you can see this takes a parameter called parent which will be assigned to self.Parent of current model. This is very important and will let us use properties and methods of parent model and other models. I will define them later in this post. Other properties and methods will be used in the page section which we have created as home. Similarly we can define the other pages with the same structure.
list.js
var listPage = function(parent) { var self = this self.Parent = ko.observable(parent) self.PageTitle = ko.observable() self.Technologies = ko.observableArray([]) self.ReInitialize = ko.observable(true) self.LoadData = function(){ self.Parent().LastPageVisited('list') if(self.ReInitialize()){ var data = [ {Id:1,Technology:"Jquery",Status:"Active"}, {Id:2,Technology:"Jquery Mobile",Status:"Active"}, {Id:3,Technology:"Knockoutjs",Status:"Active"}, {Id:4,Technology:"Knockoutjs Mapping",Status:"Inactive"}, ] self.Parent().Data(data) self.ReInitialize(false) } } self.ResetList = function () { self.ReInitialize(true) self.LoadData() } } |
And form.js
var formPage = function(parent) { var self = this self.Parent = ko.observable(parent) self.Id = ko.observable() self.Technology = ko.observable() self.Status = ko.observable() self.LoadData = function(){ self.Parent().LastPageVisited('form') self.Reset() } self.Reset = function () { self.Id('') self.Technology('') self.Status('') } self.AddItem = function () { var data = { Id : self.Id(), Technology:self.Technology(), Status:self.Status() } self.Parent().Data.push(data) return true } } |
AS we have finished defining sub models we can now see how we can call them in parent model. We need to create a simple function in parent model for this.
self.LoadData = function(){ self.HomePage(new homePage(self)) self.FormPage(new formPage(self)) self.ListPage(new listPage(self)) } self.LoadData() |
Remember when we include files using script tags first we need to load models and then parent model or else it will say undefined property homePage. We are calling this `self.LoadData()` immediately so that each property is initialized when the parent model is applied ko.applyBinding.
And now we can see complete structure of parent model.
viewmodel.js
function VM() { var self = this; self.Title = ko.observable('With Binding Magic! Single Page Application with Knockout and JQM!') self.HomePage = ko.observable() self.FormPage = ko.observable() self.ListPage = ko.observable() self.Data = ko.observableArray([]) self.LastViewedPage = ko.observable('Home') self.LastPageVisited = ko.observable() self.LoadData = function(){ self.HomePage(new homePage(self)) self.FormPage(new formPage(self)) self.ListPage(new listPage(self)) } self.LastPage = function(){ self.LastViewedPage(self.LastPageVisited()) self.HomePage().ModifyMessage(true) return true } self.LoadData() } var vm = new VM() |
The important thing to implement is how we are going to bind them in html page sections. This is pretty simple. Just we need `With` binding.
<body> <!-- /////// home page start /////// --> <section id="home" data-role="page" data-bind="with:HomePage"></section> <!-- /////// home page end /////// --> <!-- /////// list page start /////// --> <section id="list" data-role="page" data-bind="with:ListPage"></section> <!-- /////// list page end /////// --> <!-- /////// form page start /////// --> <section id="form" data-role="page" data-bind="with:FormPage"></section> <!-- /////// form page end /////// --> </body> |
And here is the final part of implementation.
$(window).on( "pagechange", function( event, data ) { var page_id = data.toPage[0].id switch(page_id) { case 'form': vm.FormPage().LoadData() break; case 'list': vm.ListPage().LoadData() break; default: vm.HomePage().LoadData() break; } }); $('document').ready(function(){ ko.applyBindings(vm) vm.HomePage().LoadData() }) |
We are applying binding on document ready. And you can note in each child model i have created `LoadData` method which initializes the child model with defaults. and also we are calling page change event and telling to call `LoadData` of each page on demand.
Now here is the complete page
<!DOCTYPE html> <html> <head> <title>Knockout! Single Page Application</title> <link rel="stylesheet" href="css/jquery.mobile-1.3.2.css" /> <link rel="stylesheet" type="text/css" href="css/style.css" /> <script type="text/javascript" src="js/jquery.js" ></script> <script type="text/javascript" src="js/knockout-3.0.0.js" ></script> <script type="text/javascript" src="js/knockout.mapping.js" ></script> <script type="text/javascript" src="js/models/home.js"></script> <script type="text/javascript" src="js/models/form.js"></script> <script type="text/javascript" src="js/models/list.js"></script> <script type="text/javascript" src="js/viewmodel.js" ></script> <script type="text/javascript" src="js/jquery.mobile-1.3.2.js"></script> <script type="text/javascript"> $(window).on( "pagechange", function( event, data ) { var page_id = data.toPage[0].id switch(page_id) { case 'form': vm.FormPage().LoadData() break; case 'list': vm.ListPage().LoadData() break; default: vm.HomePage().LoadData() break; } }); $('document').ready(function(){ ko.applyBindings(vm) vm.HomePage().LoadData() }) </script> </head> <body> <!-- /////// home page start /////// --> <section id="home" data-role="page" data-bind="with:HomePage"> <header> <nav class="v2_n"> <a href="#home" class="active" >Home</a> <a href="#list" data-transition="fade" >List</a> <a href="#form" data-transition="fade" >Form</a> </nav> </header> <section class="main_body max_600width"> <section class="selection_screen"> <div> <h1 id="" data-bind="text:$root.Title"></h1> <p data-bind="text:GreetingMessage"></p> </div> <section class="user_dtl"> <h2>You have recently visited</h2> </section> <h2 data-bind="text:$root.LastViewedPage"></h2> </section> <!-- /////// home page end /////// --> <!-- /////// list page start /////// --> <section id="list" data-role="page" data-bind="with:ListPage"> <header> <a data-bind="click:$root.LastPage" class="go_back ui-link" href="#home">Back</a> <nav class="v2_n"> <a href="#home" data-transition="fade" >Home</a> <a href="#list" class="active" >List</a> <a href="#form" data-transition="fade" >Form</a> </nav> <a data-bind="click:ResetList" class="go_next ui-link" href="#">Reset List</a> </header> <section class="main_body max_600width"> <section class="selection_screen" data-bind="foreach:$root.Data"> <article class="prfl_box"> <h3 data-bind="text:Technology"></h3> <p data-bind="text:Id"></p> <p data-bind="text:Status"></p> </article> </section> </section> </section> <!-- /////// list page end /////// --> <!-- /////// form page start /////// --> <section id="form" data-role="page" data-bind="with:FormPage"> <header> <a data-bind="click:$root.LastPage" class="go_back ui-link" href="#home">Back</a> <nav class="v2_n"> <a href="#home" data-transition="fade" >Home</a> <a href="#list" data-transition="fade">List</a> <a href="#form" class="active" >Form</a> </nav> </header> <section class="main_body max_600width"> <form> <article class="new_posts"> <h2>Id</h2> <input type="text" data-bind="value:Id"/> <h2>Technology</h2> <input type="text" data-bind="value:Technology"/> <h2>Status</h2> <input type="text" data-bind="value:Status"/> <a href="#list" class="button text_center ui-link" data-bind="click:AddItem" data-transition="slide">Save</a> </article> </form> </section> </section> <!-- /////// form page end /////// --> </body> </html> |
Demo
You can see it in action here.
Now lets do some explanation which is very important. I break it in two parts.
HTML End
Calling Properties
1. Using parent model properties in any child with html is pretty simple. We need to use $root keyword. For example
data-bind="text:$root.Title" |
in any section will let as use Title property of parent model.
2. If we want to call any property of current (self) model we can simply use it with out using any context variable.
For example while we are on home page we can display GreetingMessage like this
data-bind="text:GreetingMessage" |
3. If we ever need some sibling (other sub model) property we can still use it. For example we need Technologies of list page on Home section we can call
data-bind="foreach:$root.ListPage().Technologies" |
This way you can go any deep level or starting from $root you can access any property.
Calling Methods
Calling parent methods is not different from properties for example calling last page is done like this
data-bind="click:$root.LastPage" |
Calling self method is same as calling properties when we are on specific section for example when we are on home page and want to call
data-bind="click:LoadData" |
And for calling sibling is
data-bind="click:$root.ListPage().ResetLis()" |
Javascript End
Calling Properties
Calling child properties in parent model methods is simple. Like this you can call
self.HomePage().ModifyMessage(true) |
This way not only you can access properties but also you can modify them.
If child model like home need to access or modify some parent property it can be called like this
self.Parent().Data() |
you can assign or modify Data property. The important thing is that it will be always available in other sub models. So that they can work instead of sesstionStorage or localStorage.
If child model needs to access or modify some property of sibling you can do this
self.Parent().ListPage().ReInitialize() |
Calling Methods
For now i assume you will have a good idea how you can call parent methods in child. Like this
self.Parent().LoadData() |
And calling self methods for example if List need to reinitialize itself
self.LoadData() |
And for calling sibling methods
self.Parent().FormPage().Reset(). |
Note : When you bind some method on anchor you always need to return true or false so that navigation can be done. The other important thing is that you can always return true after setting some data for example like this
self.Parent().Data(mydata) |
Now the application is ready for Phone Gap to get hands on it.
Hope this helps. If you need some answer you can comment.