Creating Rich Interactive Web Apps With KnockOut.js – Part 4
Intro
In part 3 we built a complete working version of the Savings Goal Simulator where each view model and view mediator is nicely separated in its own module and corresponding source file. But what about the view? Well the goal of part 4 is to modularize and extract each view component.
Synopsis Of The Approach
The approach will need to meet the following requirements:
- The view markup needs to have its own url and be cacheable
- The view markup needs an ID
- The view should have an HTML element
- The view HTML element should be renderable
View Markup With A URL
Just like script blocks and images can be “sourced” externally from any url, we need a similar mechanism for our views.
<view src="http://my.view.html"> </view>
So since the script tag allow us to do that already, let’s use it, assign an id, but let’s use a more appropriate type like text/html.
<script src="http://my.view.html" type="text/html"> </script>
View Markup With An ID
To make it possible to reference the “script-view-markup” later on in our page or Javascript code, we’ll just assign the script an ID like so:
<script id="my-view-markup" src="http://my.view.html" type="text/html"> </script>
View HTML Element
Now that we have a way to define how a view markup can be “included” in our page, we’ll just define our view “container” as a normal HTML element. We can use a “div” or any of the more semantically significant HTML5 elements like “section“:
<div id="my-view"> </div>
What we’re missing is a mechanism to:
- associate the view element and the view markup together
- render the actual view
- optionally: data-bind any view model to the view
Renderable View
To render the markup for a given view we need a view rendering engine. As of this writing, the most prevalent option is jQuery Template.
Note: The jQuery Template implementation is rock solid and in use in many production web sites and internal applications. However the jQuery UI core team is in the process of deprecating its use and eventually replace it with another implementation. A serious contender for this is the jsRender + jsViews library. Other options also include Mustache and Handlebars. Although this tutorial is leveraging jQuery Template as its platform, I recommend you perform some additional due diligence once you’re ready to settle on library.
KnockoutJS goes one step further by integrating the view rendering engine with data-binding capabilities.
Note: KnockoutJS will allow pluggable view rendering engines over time, making it possible to use the one you choose (as long a as a plugin exists).
So KnockoutJS can take a view element with a corresponding “view template” and a view model, and render the resulting view.
Since we introduced the notion of “view template“, let’s spend some time getting familiar with the concept before proceeding with externalized views.
Introduction To Templating
Simple String-Based Templates
An string-based template is just a string containing text plus placeholders for variables. A placeholder is the name of the variable surrounded by curly braces and a prefixed by a dollar sign. So for example a template to display a time variable would look like:
var viewTemplate = "${firstName}, the time is now ${time}"
We can group our “variables” in a Javascript object literal like so:
var viewData = { firstName: 'Chris', time: (new Date()).toLocaleTimeString() }
We can get the engine to bind the template and the data like so:
var viewContent = $.tmpl(viewTemplate, viewData);
Finally assuming a div with the id ‘my-view’, jQuery can render the view content like so:
$('#my-view').html(viewContent);
In summary here is a diagram showing the relationships between the various components used in templating:
To try this out yourself:
- Create a web page named index.html
- Add script references to both jQuery and jQuery Template using their content distribution network urls:
- http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js
- http://ajax.aspnetcdn.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js
-
Add a div with the id ‘my-view’ in the body of the page
<div id='my-view'></div>
-
Add a Javascript script block with a jQuery ready function with our template rendering code:
<script type="text/javascript" > $(document).ready(function () { var viewTemplate = "${firstName}, the time is now ${time}"; var viewData = { firstName: 'Chris', time: (new Date()).toLocaleTimeString() } var viewContent = $.tmpl(viewTemplate, viewData); $("#my-view").html(viewData); } </script>
You should be able to open the index.html web page in a browser and the div should now contain the rendered text with the first name and the current time.
Now we have an idea of how templating using jQuery Template is working. But in this example we are just using a string to hold our template. The library actually has a feature where you can store the template in a script tag on the same page.
Simple Inline Templates
The jQuery Template library also allow us to store the template in a script tag with a text/html type like so:
<script id='my-view-template' type='text/html'> ${firstName}, the time is now ${time} </script>
The template can then be rendered with a slightly different form of the tmpl API:
var viewContent = $('#my-view-template').tmpl(viewData);
Having the template moved out of the code is a step in the right direction, but what we need is the ability to externalize the template to an external url. We’ll tackle that subject soon I promise, but in the meantime let’s look at what KnockoutJS brings us in terms of additional templating power.
KnockoutJS Databinding Support For Inline Templates
Up to now, we have had to write the actual code to: a) render the template, and then b) insert the resulting text in an HTML element. Well, let’s see what KnockoutJS can do to simplify our task. (This will be very handy especially when your main page has many views.)
As you know, KnockoutJS provides a “data-bind” attribute where you can specify the visual or behavioral aspects (e.g. value, text, visible, enable, …) to link to a value model or dependent observable function. When we want to data-bind an HTML element to both a template and a view model, we’ll use the following syntax:
<div id='my-view' data-bind='template: { name: "my-view", data: myViewModel }'> </div>
It is even possible to specify a Javascript function in an afterRender template parameter. That function will the be invoked once the HTML element has been rendered. This can be useful if you need to dynamically attach new behaviors or event handlers to the produced content. For example, you could invoke jQuery UI component configuration APIs such as applying icons for a jQuery UI button.
<div id='my-view' data-bind='template: { name: "my-view", data: myViewModel, afterRender: myViewHookup }'> </div>
Our view model would then be re-written as follows:
var myViewModel = { firstName: ko.observable('Chris'), time: ko.observable((new Date()).toLocaleTimeString()) }
And our “myViewHookup” after-render function would have the following form:
function myViewHookup(renderedElements, viewModel) { // E.g. highlight the new content for 3 seconds renderedElements.effect('highlight', { color: 'LightGreen' }, 3000); }
And finally our page initialization code would ask KnockoutJS to apply the data bindings using the ko.applyBindings API:
$(document).ready(function () { var myViewModel = { firstName: ko.observable('Chris'), time: ko.observable((new Date()).toLocaleTimeString()) } ko.applyBindings(viewModel); });
So when using jQuery, jQuery Template, and KnockoutJS, we can define a template-based view and data-bind it to a view model so it can be rendered dynamically.
Now we have covered all but one requirement: the ability to externalize the view template to a specific url.
Externalized View Templates
Basic Mechanism Needed To Externalize A Template
Let’s review our simple “template script markup”:
<script id='my-view-template' type='text/html'> ${firstName}, the time is now ${time} </script>
What we really need at this point is a way to externalize this template into its own file (maybe in a sub-folder named after the parent page, itself organized under a “view” folder):
/view/mypage/my.view.html |
---|
${firstName}, the time is now ${time} |
We would then just add a src tag to our script block:
<script id='my-view-template' type='text/html' src='/view/mypage/_my.view.html' > </script>
If you try this out you will notice that the browser will indeed [wget] “fetch” the content of _my.view.html, and using the web developer tool of your browser you will actually see the template markup.
But if you try to access the content of the script element using jQuery using:
$("#my-view-template").text()
… you will notice that the content is empty! The browser engine has retrieved the content but did not store it in the actual DOM element for the script. This is due to the fact that a script of type text/html is not recognized as being a script to interpret.
So we need a mechanism to fetch and store the source. The approach consists of re-fetching the source using jQuery’s $.get API. Since it was fetched once, the content is stored in the browser’s cache so retrieval wil be very fast. We then store it in the DOM’s element.
Here is a simplistic example of what the code would look like:
$(document).ready(function () { $.get("/view/mypage/_my.view.html", function(data, textStatus, jqXHR) { $("#my-view-template") .text(jqXHR.responseText); } ); });
So finally we can integrate our KnockoutJS view model code in the ready function:
$(document).ready(function () { $.get("/view/mypage/_my.view.html", function(data, textStatus, jqXHR) { $("#my-view-template") .text(jqXHR.responseText); var myViewModel = { firstName: ko.observable('Chris'), time: ko.observable((new Date()).toLocaleTimeString()) } ko.applyBindings(viewModel); } ); });
You now have externalized the view template, dynamically loaded it, data-bound it to a view model using KnockoutJS, and rendered using the jQuery Template engine!
Our example was fairly basic but it illustrates the overall concept and approach.
Generic ViewLoader Plugin
The jQuery ViewLoader plugin will take us even further by providing the ability to:
- Handle nested externalized templates
- Parallelize the processing of each template
- Provide a simple way to handle all templates on a page and synchronize their post-processing so that the page initialization logic can proceed.
Here is what the minimum syntax looks like:
$(document).viewloader({ logLevel: "debug", success: function () { ko.applyBindings(viewModel); } });
The plugin will identify all scripts of type text/html and a source url ending in “.view.html“, including nested template scripts and request their parallel loading. Once all template scripts have been succesfully processed the success function is invoked.
Behind the scenes, jQuery ViewLoader relies on the jQuery concept of “Deferred“, an action which can be launched for “deferred” (i.e. later) execution, but can trigger a callback when completed. Actions can be grouped together so that a specific callback can be triggered once all of them have completed.
Here is a diagram of the approach:
Externalizing The Views Of Our Savings Goal Simulator
Intro
We can now apply the new approach to our Savings Goal Simulator web app since it currently contains 3 views:
- savings-goal-view
- consumption-scenarios-view
- savings-forecast-view
For each view, we will:
- Create an externalized template in the /view/index folder based on the view markup
- Replace the view in index.html by a template script reference and a container DIV data-bound to the template.
Finally, we’ll update the main application.js to plugin the jQuery ViewLoader.
Here is an outline of the step-by-step approach:
# | To Do | Area |
---|---|---|
1 | Create An External Template For The savings-goal-view | View |
2 | Refactor the savings-goal-view In Our Web Page | View |
3 | Hookup jQuery ViewLoader In Our Main application.js | Application |
4 | Refactor The Savings Goal Mediator | View Mediator |
1. Create An External Template For The savings-goal-view
Our first step will be to create a new folder named view under scripts to organize our external views. Similarly to frameworks like Ruby On Rails, or ASP.NET MVC, we’ll create a subfolder named shared, for all views reusable across pages, then we’ll create a subfolder named index for views appearing on our index.html web page.
In the scripts/view/index folder, let’s create a file named _savings-goal.view.html. I used an underscore prefix to reflect the Rails convention of “partial views” being prefixed that way.
Then we can copy the savings-goal-view markup from index.html page and paste it in our new _savings-goal.view.html file.
<section id='savings-goal-view' > <label for="savings-goal-amount">Savings Goal Amount:</label> <input id="savings-goal-amount" /><br/> <label for="savings-max-duration">Savings Max Duration (In Months):</label> <input id="savings-max-duration" maxlength="3" /><br/> <label for="savings-target-per-month">Savings Target Per Month:</label> <span id="savings-target-per-month" /><br/> </section>
Now we can refactor index.html.
2. Refactor the savings-goal-view In Our Web Page
In place of our savings-goal-view markup, let’s first add a script reference to the view we just created, and give it an id of savings-goal-view-template:
<script id='savings-goal-view-template' type='text/html' src='scripts/view/index/_savings-goal.view.html' > </script>
Second, let’s add a div acting as a container for our external view. We’ll give it an id of savings-goal-view-container, and declare our KnockoutJS data-binding with the savings-goal-view-template:
<div id='savings-goal-view-container' data-bind='template: { name: "savings-goal-view-template" } ' > </div>
Let’s download:
- the latest version of jQuery Templates from https://github.com/jquery/jquery-tmpl
- the latest version of KnockoutJS from https://github.com/SteveSanderson/knockout
- the jQuery ViewLoader plugin and place it in the /scripts/vendor folder.
In the index.html page, in the LAB loader section, let’s:
- Replace the call to load jQuery Template from our downloaded version (instead of the version from the CDN which is older)
- Replace the call to load KnockoutJS from our downloaded version
-
Add a call to load the jQuery ViewLoader plugin:
<script type="text/javascript"> $LAB .script("http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js").wait() // ... .script("scripts/vendor/jquery.tmpl.min.js").wait() .script("scripts/vendor/knockout-latest.debug.js").wait() // ... .script("scripts/vendor/jquery.viewloader.js").wait() // ... </script>
Here is how all the piece parts will fit together:
Ok, now it’s time to plug in code to get the ViewLoader to load our view.
3. Hookup jQuery ViewLoader In Our Main application.js
First let’s create 2 new utility functions in application.js:
- GetPageSettings – to retrieve our page settings from the DOM
-
SetPageSettings – to store our page settings in the DOM
function GetPageSettings() { return $(document).data("sgs.pagesettings"); } function SetPageSettings(pageSettings) { $(document).data("sgs.pagesettings", pageSettings); }
Let’s review the InitializeApplication function of our main application.js module. The core code consists of initializing a pageSettings dictionary and invoking our view mediators.
Let’s extract that part of the code into a new function named InitializeViewMediators:
function InitializeViewMediators() { if (typeof(console) != 'undefined' && console) console.info("InitializeViewMediators starting ..."); // Initialize our page-wide settings var pageSettings = { defaultSavingsGoal: 500, defaultSavingsMaxDuration: 6 } // Save the page-wide settings SetPageSettings(pageSettings); // Create / launch our view mediator(s) sgs.mediator.savingsgoal.createViewMediator(pageSettings); sgs.mediator.coffeepricing.createViewMediator(pageSettings); sgs.mediator.consumptionscenarios.createViewMediator(pageSettings); sgs.mediator.savingsforecast.createViewMediator(pageSettings); if (typeof(console) != 'undefined' && console) console.info("InitializeViewMediators done ..."); }
Let’s add the code to InitializeApplication to attach our ViewLoader plugin to the document object and invoke InitializeViewMediators upon success (i.e. once all views have been loaded):
function InitializeApplication() { if (typeof(console) != 'undefined' && console) console.info("InitializeApplication starting ..."); $(document).viewloader({ logLevel: "debug", success: function (successfulResolution) { InitializeViewMediators(); } ); if (typeof(console) != 'undefined' && console) console.info("InitializeApplication done ..."); }
And let’s add some logging code in the error function to know when the ViewLoader is failing to load a given view.
function InitializeApplication() { // ... $(document).viewloader({ // ... success: function (successfulResolution) { InitializeViewMediators(); }, error: function (failedResolution) { if (typeof(console) != 'undefined' && console) { console.error("index.html page failed to load"); } } }); // ... }
Here is the visual depiction of how application.js coordinates with the ViewLoader:
Not so fast … if we run the app, we’ll get an error. This is due to the fact that the code from the createViewMediator function of our sgs.mediator.savingsgoal module is expecting the view to be available. But at this point in the execution the view as not yet been rendered. This leads us to one more refactoring.
4. Refactor The Savings Goal Mediator
The current implementation of the createViewMediator function of our sgs.mediator.savingsgoal module assumes that our view is in the DOM, but at the moment the ViewLoader plugin kicked off the InitializeViewMediators function, KnockoutJS has not been requested to apply bindings yet nor render any view templates.
So let’s refactor the code related to setting up the various data-bindings and invoking KnockoutJS into a new function named setupViewDataBindings:
sgs.mediator.savingsgoal.setupViewDataBindings = function() { // Declare the HTML element-level data bindings $("#savings-goal-amount").attr("data-bind","value: savingsGoalAmount"); $("#savings-max-duration").attr("data-bind","value: savingsMaxDuration"); $("#savings-target-per-month").attr("data-bind","text: savingsTargetPerMonthFormatted()"); // Ask KnockoutJS to data-bind the view model to the view var viewNode = $('#savings-goal-view')[0]; var viewModel = sgs.mediator.savingsgoal.getViewModel(); ko.applyBindings(viewModel, viewNode); // Apply masking to the savings goal amount and max duration input fields viewModel.savingsGoalAmountMask.attach($("#savings-goal-amount")[0]); viewModel.savingsMaxDurationMask.attach($("#savings-max-duration")[0]); // Initialize default for value models linked to masked fields var pageSettings = GetPageSettings(); viewModel.savingsGoalAmount(pageSettings.defaultSavingsGoal || 0); viewModel.savingsMaxDuration(pageSettings.defaultSavingsMaxDuration || 0); // Subscribe to interesting value model changes viewModel.savingsTargetPerMonthFormatted.subscribe(function() { $("#savings-target-per-month") .effect('highlight', { color: 'LightGreen' }, 3000); // for 3 seconds }); if (typeof(console) != 'undefined' && console) console.info("sgs.mediator.setupViewDataBindings done!"); }
Note: We had to add a new line to access the viewModel since it is now available in the DOM and accessible using the sgs.mediator.savingsgoal.getViewModel function. We also add to add a line to get the pageSettings using our new GetPageSettings function.
After having extracted some of the code the createViewMediator function consist of the following:
sgs.mediator.savingsgoal.createViewMediator = function (pageSettings, pageViewModel) { // Create the view Savings Goal view-specific view model var viewModel = sgs.model.savingsgoal.initializeViewModel(pageSettings); // Save the view model sgs.mediator.savingsgoal.setViewModel(viewModel); if (typeof(console) != 'undefined' && console) console.info("sgs.mediator.savingsgoal ready!"); }
But since we added the savings-goal-view-container div to the page, we need to handle its data binding here. So let’s do that:
sgs.mediator.savingsgoal.createViewMediator = function (pageSettings, pageViewModel) { // Create the view Savings Goal view-specific view model var viewModel = sgs.model.savingsgoal.initializeViewModel(pageSettings); // Save the view model sgs.mediator.savingsgoal.setViewModel(viewModel); // Ask KnockoutJS to data-bind the view model to the view var viewNode = $('#savings-goal-view-container')[0]; ko.applyBindings(viewModel, viewNode); if (typeof(console) != 'undefined' && console) console.info ("sgs.mediator.savingsgoal ready!"); }
You probably noticed that there is no call yet to invoke our new setupViewDataBindings function! So where will it be invoked then? Well do you remember the comment about KnockoutJS exposing an afterRender callback function? (See back in KnockoutJS Databinding Support For Inline Templates.)
We’ll update the data-binding of savings-goal-view-container in our index.html, by assigning our new sgs.mediator.savingsgoal.setupViewDataBindings function to the afterRender attribute:
<div id='savings-goal-view-container' data-bind='template: { name: "savings-goal-view-template", afterRender: sgs.mediator.savingsgoal.setupViewDataBindings } ' > </div>
And now if we re-run our application, we’ll see in the console that setupViewDataBindings is being called while the savings-goal-view is being rendered, after the loading is successfully completed.
Here is an excerpt of the console:
InitializeApplication starting ... viewloader.loadSourceForPartialView loading source for view: id=savings-goal-view-template src=scripts/view/index/_savings-goal.view.html InitializeApplication done ... viewloader.loadSourceForPartialView saving source in savings-goal-view-template viewloader.loadAllPartialViews executing 'success' function InitializeViewMediators starting ... sgs.mediator.setupViewDataBindings done! sgs.mediator.savingsgoal ready! ... InitializeViewMediators done ...
We have successfully externalized the markup for the savings-goal-view!!!
In summary here is a diagram illustrating the page load processing flow:
5. Recap
Here is a quick recap of the steps we followed:
# | To Do | Area |
---|---|---|
1 | Create An External Template For The savings-goal-view
|
View |
2 | Refactor the savings-goal-view In Our Web Page
|
View |
3 | Hookup jQuery ViewLoader In Our Main application.js
|
Application |
4 | Refactor The Savings Goal Mediator
|
View Mediator |
6. Externalizing The Remaining Views
Now we just need to repeat the process on our remaining two views:
- consumption-scenarios-view
- savings-forecast-view
If you want to follow along with the source code changes, just get code for the tag corresponding to any particular step on the GitHub repository for the Savings Goal Simulator.
So What?
jQuery Template and jQuery ViewLoader allow you to structure and load views within a web page.
Externalizing your views makes your web pages more modular and easier to maintain, especially if you are working within a team as this allows you to assign different views (markup + code) to different individuals.
In the context of a “single page application” where you may have many views, these benefits are crucial to help you manage the complexity and allow you to grow the application organically / incrementally.
Another benefit of modularizing your views and view mediators will be that it will be easier to adapt and create different versions of the application for other channels, like a mobile web site or a mobile version of the application (using for example jQuery Mobile and PhoneGap).
References And Resources
jQuery Plugins
- jQuery Template : http://api.jquery.com/category/plugins/templates/
- jQuery ViewLoader : http://github.com/techarch/jquery-viewloader
- jsRender + jsViews rendering engine : https://github.com/BorisMoore/jsviews
- Mustache rendering engine : http://mustache.github.com/
- Handlebars rendering engine : http://www.handlebarsjs.com/
- jQuery ViewLoader rendering engine : https://github.com/techarch/jquery-viewloader
Miscellaneous
- jQuery $.get : http://api.jquery.com/jQuery.get/
- jQuery Deferred : http://api.jquery.com/category/deferred-object/
- KnockoutJS afterRender : http://knockoutjs.com/documentation/template-binding.html#note4usingthe_option
- Savings Goal Simulator : http://savings-goal-simulator.heroku.com/
My Other Related Posts:
- Creating Rich Interactive Web Apps With KnockOut.js – Part 1
- Creating Rich Interactive Web Apps With KnockOut.js – Part 2
- Creating Rich Interactive Web Apps With KnockOut.js – Part 3
Full Source Of The Savings Goal Simulator (KnockoutJS Demo)
The whole application is available on Github under techarch / savings-goal-simulator.
A very interesting article. Would be great to have an example of how to use the modules. E.g. I need to reuse some view in another page with some other views. How it would be done? As one of the adventages is reusability, that would be nice to see it in action.
Comment by Yaroslav | January 4, 2012
This is a brilliant, easy to follow tutorial which shows a complete solution how to use knockout.js effectively. Mediators' role in this solution is great since it makes the html clean. Thanks!
Comment by @Jarmo_Kristian | January 6, 2012
Thanks Philippe. Excellent in depth tutorial. Not just on Knockout but on client architecture in general.
Those using chrome and downloading the source – index.html will not load correctly from the file system. It will give error when loading the html templates (Origin null is not allowed by Access-Control-Allow-Origin). However, don't despair. You can run it as a web app and it will be fine. See the article: http://stackoverflow.com/questions/5224017/origin…
(Could not get the other solutions from http://stackoverflow.com/questions/3595515/xmlhtt… to work…)
Comment by Scott Prohaska | January 13, 2012
Would be awesome if you would have an article on developing a mobile application using jQuery Mobile and PhoneGap, using the architecture you suggested.
Comment by Andrew | July 11, 2012
Andrew, it's something I have wanted to do for a while – so one day I will tackle that :-). I have actually created several Cordova (Phonegap) apps using the architecture described in the blog. The modularization has paid off greatly.
Comment by techarch | September 16, 2012
Hey techarch – thanks so much for this – i've been using it with much success! Love the detailed approach.
I have a question for you, i'm trying to center a component onscreen, but I noticed it doesn't work when the html is externalized in the _view. However when I move it into the index.html it works. I inspected the source for both, and I suspect the former isn't working because it's not really "in" the DOM as you point out… do you have any idea how I might be able to fix that? thanks! Bart
Comment by Bart | July 28, 2013