Introduction
SPA as the name suggests, is an application where all associated pages of any application are laid down over a single web page. In another words, the entire necessary resources such as HTML page, stylesheet, JavaScript files, of any application are loaded in the browser after the first request and rendered when requested.
There are many frameworks available in the market to build SPA but here in this article, we are going to cover the Durandal SPA framework.
Durandal
In simple words, it is a combination of different standard JavaScript libraries such as Knockout, RequireJS, jQuery, which helps us to build less time taking, responsive UI for better user experience.
Background
When I was implementing client side framework, I faced a number of problems with the configuration of view with view models, knockout.js, require.js, CSS, and many more things. Then I found Durandal, a framework which provides all the necessary configurations which are required to start a new project without putting in much effort with respect to configuration.
Implementations
In general, Single Page Application, client side data model connects to the service layer for sending and receiving JSON data by using Ajax call.
In this demo, we are not going to touch any server side component; rather we would be completely focused on client side component.
Summary of Implementation
Going to make a simple menu controls with sub menu options; which will load view dynamically on each submenu click.
Going to Learn
- Import Durandal framework to Web app
- Create view and view model
- Bind view to the respective view model
- Bind view and view model with dynamic data
- Load views dynamically when necessary
- Interact different to view with single page
Setup the Solution
Step 1
Create a solution named Demo.SPA.Solution
.
Step 2
Add a new project ASP.NET Web Application and select empty- MVC template.
The solution will appear as below:
Step 3
Add Durandal starter kit to web App:
It will add some additional folders such as App, App\Views, App\ViewModels and dependent files, durandalconfid.cs, durandalBundle.config, DurandalController
.
My solution will appear as:
Note: shell.js under App\ViewModels will be used to define routing.
Step 4
Build and run the solution.
Since durandalController
is the default controller, browse the url as:
http://localhost:XXXX/durandal
The webpage will appear as:
Welcome and flicker tabs comes with default Durandal framework.
To remove this table, you need to comment the below lines in shell.js.
{ route: '', title:'Welcome', moduleId: 'viewmodels/welcome', nav: true },
{ route: 'flickr', moduleId: 'viewmodels/flickr', nav: true }
Using the Code
Going forward to our sample application.
Step 5
Add view (home.html, menu.html, productlist.html, productdetails.html, addnewproduct.html) inside App/views folder.
Step 6
Add viewmodel
s with same name as HTML (e.g.: home.js, menu.js..) inside app/viewmodels folder.
I have created a folder called Repository, which will take care of the data part, since we are not including any server side implementation in this demo.
Inside Repository folder, I have created two JS files, name BookRepository.js and MenuRepository.js.
Inside the script
folder, I have created globaldata.js file, where I will declare global scope variables.
And bundle it to DurandalBundle.config.cs file by using the following line:
Include("~/Scripts/globaldata.js")
Hence, my solution is appearing now as:
Using the Code
For getting the data:
In BookRepository.js and MenuRepository.js, I have defined some variables and methods which we will use further down the application.
BookRepository.js
define(function (require) {
return {
_books: [
{ id: 0, title: 'The Low Land', writter: 'Jumpa Lahiri',
price: '12', description: 'Test low land description' },
{ id: 1, title: 'The Story of time being', writter: 'Ruth Ozeki',
price: '13', description: 'Test Story of time being description' },
{ id: 2, title: 'Alchemist', writter: 'Paulo',
price: '14', description: 'Test Story of time being description' },
{ id: 3, title: 'The Narrow Road to the Deep North',
writter: 'Richard Flanagan', price: '10',
description: 'Test Narrow Road to the Deep North description' },
{ id: 4, title: 'Luminaries', writter: 'Eleanor Catton',
price: '11', description: 'Test Luminaries description' },
{ id: 5, title: 'Sense of an Ending', writter: 'Julian Barnes',
price: '12', description: 'Test Sense of an Ending description' } ],
listBooks: function () { return this._books; },
getBooksById: function (id) {
for (var i = 0; i < this._books.length; i++) {
if (this._books[i].id == id) {
return this._books[i];
break;
}
}
}
};
});
MenuRepository.js
define(function () {
return {
_menusItems: {
menu: [
{ name: 'Home', link: '0', sub: null },
{
name: 'Products', link: '1',
sub: [{ name: 'List of Products', sub: null },
{ name: 'Register New Product', sub: null },
{ name: 'Enquiry Product', sub: null }]
},
{
name: 'About US', link: '2', sub: [{ name: '', sub: null },
{ name: '', sub: null }]
},
{
name: 'Contact', link: '3',
sub: [{ name: 'Corporate Office', sub: null },
{ name: 'Home Office', sub: null }]
}
]
},
menuItems: function () { return this._menusItems; }
}
})
Step 7
Now we are going to work on our menu view model.
Since we are taking menu data from MenuRepository.js, so for fetching menu data in menu view model, first we need to load MenuRepository
, which we will do by declaring it inside define box such as:
define(['Repository/MenuRepository'], function (mRepository)
By doing this, we can access the variables and functions of MenuRepository.js to this page.
Menu data we will store in it observable array and bind it to the view.
Hence, respective data will appear on the web.
Menu.js
define(['Repository/MenuRepository'], function (mRepository) {
var menuConstructor = function () {
var self = this;
g_menuItemObservable = ko.observable("");
self.menuItems = ko.observableArray([]);
self._menus = [];
self.getMenu = function () {
self._menus = mRepository.menuItems();
}
self.attached = function (view) {
self.getMenu();
self.menuItems(self._menus.menu);
}
}
return new menuConstructor();
})
Menu.html
<div class="menu-style" style=" height:80%; width:70%; margin-left:10%; margin-top:2%;">
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div>
<ul class="nav navbar-nav" data-bind="foreach:menuItems">
<li class="dropdown">
<a class="dropdown-toggle"
data-toggle="dropdown" data-bind="text:name"></a>
<ul class="dropdown-menu" data-bind="foreach: sub">
<li data-bind="text:name"></li>
</ul></li>
</ul>
</div></div>
</nav>
</div>
View model follows some life cycle. Here, I am using composition life cycle (activate\attach according to my requirement).
For life cycle, please check durandal docs.link
(http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.html).
In the same way, we are going to create productlist
view and view model.
Productlist.js
define(['Repository/BookRepository'], function (repository) {
var productConstructorViewModel;
var productConstructor = function () {
var self = this;
productDetailObservable = ko.observable("");
self.products = ko.observableArray([]);
self._products = []
self.getProducts = function () {
self._products = repository.listBooks();
self.products(self._products);
};
self.activate = function (data) {
self.getProducts();
}
self.removeProfile = function (list) {
if (confirm("Are you sure you want to delete this profile?")) {
self.products.remove(list);
}
}
self.editProfile = function (list) {
}
self.getList = function (data, event) {
g_productID = data.id;
$('#divProductdetails').css("display", "block");
}
}
return new productConstructor();
})
Productlist.html
<div><div>
<table class="table table-striped table-bordered table-condensed" >
<tr><th>Title</th><th>Author</th></tr>
<tbody data-bind="foreach:products">
<tr><td><a data-bind="text:title"></a></td>
<td data-bind="text:writter"></td>
<td><button class="btn btn-mini btn-danger"
data-bind="click: function(data, event) {$root.getList(data, event);
return true;}">Details</button></td>
<td><button class="btn btn-mini btn-danger"
data-bind="click: $parent.removeProfile">Remove</button></td>
<td><button class="btn btn-mini btn-danger"
data-bind="click: $parent.editProfile">Edit</button></td></tr>
</tbody>
</table>
</div>
Now, I am going to bind menu with home
and productlist
with menu.
Here, we are using an important knockout binding concept called compose
.
Home.js
define([], function () {
var homeconstructorViewModel;
homeconstructorViewModel = function () {
self = this;
self.menuItemObservable = ko.observable("");
self.categories = ["Product List"];
self.productListObservable = ko.observable("");
self.loadProductList = function (data, event) {
self.productListObservable({ view: 'views/productlist.html',
model: 'viewmodels/productlist' });
$('#divProducts').css("display", "block")
}
$(document).on("click", ".dropdown-menu li", function (e) {
var ctrl = $(this).text();
if (ctrl == "List of Products") {
self.menuItemObservable({ view: 'views/productlist.html',
model: 'viewmodels/productlist' })
}
if (ctrl == "Register New Product") {
self.menuItemObservable({ view: 'views/addnewproduct.html',
model: 'viewmodels/addnewproduct' })
}
})
}
return homeconstructorViewModel;
});
Home.html
<div>
<div style="height:80%; width:70%"><div>
<div data-bind="compose:
{ model:'viewmodels/menu', view:'views/menu.html'}">
</div> </div> </div>
<div>
<div id="divMenuItemBody" style="margin-left:10%;
margin-top:2%; width:70%; height:40%">
<div data-bind="compose: menuItemObservable" style="height:100%;"></div>
</div> </div>
</div>
Here, in HTML, you can see that I have used two data-bind, in the first case compose is using static
defined view and view model, however in the second one, it's observable which loads view\view model dynamically whenever observable variable value changes (check in home.js).
I have also designed the rest of the pages by using the same kind of logic, you can check by downloading the sample.
Step 8
Now come to the routing section.
I want to browse the home page on the page load so will define the routing as below in shell.js file.
{ route: 'home', title: 'Demo', moduleId: 'viewmodels/home', nav: true }
Points of Interest
In this article, we have discussed about how to use durandal in web app.
I am writing this article by keeping beginners in my mind.
Hope this article will be fruitful for you.
Source Code
I have uploaded this sample project with some more features along with this article. You can just download this sample code and make your understanding better on these concepts and implementation further.
Use the following URL to run the application.
http://localhost:XXXX/durandal
XXXX is the port number of your local system on which the application is running.
History
- 7th January, 2016: Initial version