SPA as name suggests, it’s an application where all associated pages of any application are lay down over a single web page. In another words the entire necessary resources such as html page, style sheet, java script files, of any application is loaded in the browser after first request and rendered when requested.
There are many frameworks available in market to build SPA but here in this article we are going to cover Durandal SPA framework
Durandal:-
In simple words, it is a combination of different standard java script libraries such as Knockout, RequireJS, jQuery, which helps us to build less time taking, responsive UI for better user experience.
Background
when I was implementaing clien side framework then faced a number of problems with the configuration of view with view models, knockout.js, require.js, css and many more thing. Then found durandal, a framework which provides all nacessary configurations which is require to start a new project without putting much effort with respect to configuration.
Implementations
In generally Single page application, client side data model connect 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 focus 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.
Step1. Create a solution name Demo.SPA.Solution.
Step2. Add new project ASP.NET Web Application and select empty- mvc template
Solution will appear as below:
Step3. Add Durandal starter kit to web App
It will add some additional folder 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.
Step4. Build and run the solution
Since durandalController is default controller so browse the url as
webpage will appear as
Welcome and flicker tabs comes with default durandal framework
To remove this tabe 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.
Step5. Add view (home.html, menu.html, productlist.html, productdetails.html,addnewproduct.html) inside App/views folder
Step6. Add viewmodels with same name as html (eg: home.js, menu.js..) inside app/viewmodels folder.
I have create a folder called Repository, which will take care of data part, since we are not including any server side implementation in this demo.
Inside Repository folder have created two JS files name BookRepository.js and MenuRepository.js
Inside the script folder have create globaldata.js file, where I will declare global scope variables
And bundle it to DurandalBundle.config.cs file by using 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 variable and method 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; } } })
Step7. 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 declare 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 obserbable 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 follow some life cycle. Here I am using composition life cycle (activate\attach according to the my requirement)
For life cycle please check durandal docs.link (http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.html)
The same way we are going to create productlsit 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) { // Implement your logic } 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 I have used two data-bind, in the first case compose is using static defined view and view model , however in the second one its observable which load view\view model dynamically whenever observable variable value changes (check in home.js).
Rest of the pages I have also designed by using the same kind of logic, you can check by downloading sample.
Step8. Now come to the routing section
I want to browse 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 }
No comments:
Post a Comment