The "Tech. Arch."

Architecting Forward ::>

Creating Rich Interactive Web Apps With KnockOut.js – Part 2

Intro

In Part 1 of “Creating Rich Interactive Web Apps With KnockOut.js”, I reviewed some of the key patterns you need to consider such as: MVC, Model View – ViewModel, Observer, Publish Subscribe, Value Model, Data Binding.

In this second post, we will cover the following topics while creating a fun app:

  • Architecting our application so it can easily be extended over time
  • Splitting the logic according to separations of concerns
  • Incorporating rudimentary elements of usability

For The Price Of N Cups of Coffee …

To illustrate the various concepts, we’ll create a simulator application to figure out how we can tweak our coffee consumption habits to save a given amount of money over a period of time.

Note: You can try the app at: savings-goal-simulator.heroku.com and inspect / play with it in Firebug. The full source is also available on Github under techarch / savings-goal-simulator.

Building Our App Step-By-Step

Here is an outline of the approach:

# To Do Area
1 Create a Shell Application Main app
2 Organize our Application Code Main App
3 Create the Savings Goal View View
4 Create the Savings Goal ViewModel View Model
5 Create the Savings Goal View Mediator View Mediator
6 Link the Page and the View Mediator Main App
7 Apply Data Masking Business Rules View Mediator
8 Apply Custom Masking Rules View Mediator
9 Applying Formatting Rules View Mediator
10 Providing Visual Feedback Cues View Mediator

Thanks to Susan Potter‘s suggestion of tagging the source code on Github, you can download the source corresponding to the application as of the end of each “todo” step! You can find the list of tags here.

1. Create a Shell Application

Note: In this tutorial we will de-emphasize the server framework side and focus on browser considerations.
So let’s start:

  1. Create a root folder for our application named savings-goal-simulator
  2. Create a file named index.html for our main page
  3. Create a folder for our Javascript files named scripts
  4. Create a sub-folder under scripts for our vendor Javascript libraries named vendor
  5. Add the basic markup for a page in index.html

Ok, now we’re ready to add our key external Javascript libraries.
Here is the basic minimum we’ll need with a quick rationale as of why they will help us:

Library Purpose / Rationale
Modernizr Detects HTML 5 support and provide IE shims
jQuery Main foundation (do I need to say more :-) ?!)
jQuery UI Prefabricated UI super-elements like tabs, accordions, sliders, etc.
jQuery Cookies Easy management of cookies
jQuery HotKeys Handling of keyboard shortcuts
jQuery Masked Input Allow creation of custom input masks
jQuery Validate Validation framework
Accounting.js Allow formatting currency amounts
CurrencyMaskJS Allow masked input of currency amounts
jStorage Wrapper for local storage APIs

If we want to start authoring HTML 5 semantic pages, Modernizr is a library which can facilitate detection of specific HTML5 and CSS3 features. But it also shines for Internet Explorer versions earlier than 9 by plugging in “shims“, i.e. fake HTML elements in the DOM, named after the new HTML5 missing elements, making it possible to use tags likeheader, footer, section, etc. and styling them too.

You will need to create your own custom version of Modernizr based on the features you need. This will ensure the smallest most effective script for your situation.
For our app, I chose to only include the “HTML5 Shim/IEPP” and the “CSS classes” – see this download configuration. I renamed the resulted file as modernizr.custom.min.js and placed it in the scripts/vendor folder.
So we’ll need to include the script for Modernizr right at the beginning our our HEAD section.

<head>
	<!-- Important: Modernizr must be the very first script in HEAD -->
	<script src="scripts/vendor/modernizr.custom.min.js"></script> 
	
	<!-- ... -->
</head>	

For performance reasons, we’ll use a Javascript loader (see available options further on) to ensure the Javascript libraries we’ll need can be loaded asynchronously, in parallel, and not block the execution of our page load.
For this app, I chose LABjs as it is specializes only on loading and is commonly used.
But note that you could use some of the conditional loading features of Modernizr, called yepnope.
I chose to keep things minimal for our app. ;-)

Starting from a basic HTML page, let’s add a script reference to lab.js below our modernizr script, and then let’s define the order in which we want to load our libraries.
We’ll add a call to wait() if we want the execution of the actual code to be order-dependent:

<head>
	<!-- Important: Modernizr must be the very first script in HEAD -->
	<script src="scripts/vendor/modernizr.custom.min.js"></script> 
	<script src="LAB.js"></script> <!-- Important: must be the very first script in HEAD -->
	<script>
	$LAB
		.script("http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js").wait()
		.script("http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js")
		.script("http://ajax.aspnetcdn.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js")
		.script("scripts/vendor/knockout-1.2.1.debug.js").wait()
		.script("scripts/vendor/jquery.cookie.js")
		.script("scripts/vendor/jquery.blockUI.js")
		.script("scripts/vendor/jquery.hotkeys.js")
		.script("scripts/vendor/jquery.maskedinput-1.3.min.js")
		.script("scripts/vendor/jquery.validate.min.js")
		.script("scripts/vendor/accounting.min.js")
		.script("scripts/vendor/jstorage.min.js")
		.script("scripts/vendor/raphael-min.js").wait()
		.script("scripts/vendor/g.raphael-min.js").wait()
		.script("scripts/vendor/g.line-min.js").wait()
		.script("scripts/application.js").wait(function(){
			// When ALL scripts have been loaded AND executed:
			InitializeApplication();
		});
	</script>
	
	<!-- CSS and other HEAD-specific tags -->
</head>	

You will notice that I am assuming a specific directory structure for our locally hosted Javascript code

  • scripts is the root of all .js files
    • Vendor will host external vendor libraries not available on a CDN.
      Having all external JS libraries in a dedicated folder will keep our directory structure clean.
    • ViewMediator will include Javascript code mediating interactions between Views and ViewModels
    • ViewModel will include our ViewModel-specific Javascript code
    • application.js will be the main application .js file with an InitializeApplication function (called from our $LAB configuration)

At this point go ahead and create the same folder structure and download the needed Javascript libraries for the Vendor folder.
You can use the links in the External Javascript Libraries table above.

And create the application.js file under the root of the scripts folder, and let’s provide a minimal implementation for now:

function InitializeApplication() {
	if (typeof(console) != 'undefined' && console) 
		console.info("InitializeApplication starting ...");
}

If you run the application you should see the informational message being displayed in the console.
And in Firefox Firebug, or on a WebKit browser you should see the parallel loading of the scripts.

For our CSS, let’s create a folder named css in which we’ll extract the custom-theme folder from our jQuery UI download.
We’ll also create an application.css file for our application-specific styles.

<link href="css/custom-theme/jquery-ui-1.8.16.custom.css" rel="stylesheet" type="text/css"  />
<link href="css/application.css" rel="stylesheet" type="text/css"  />

2. Organize our Application Code

Since our application will very modular and encompass many functions or classes,
we’ll establish a namespace to provide some structure and avoid function name clashes.
You can read more about how to implement namespaces in Javascript in the following two articles:

For this app, we’ll use sgs (for savings goal simulator) as a prefix,
followed by either model or mediator for ViewModel and ViewMediator respectively.
To keep our markup free of Javascript logic, we’ll organize the application code in the following folders:

  1. application.js will contain only logic to initialize / setup the views and viewmodels
  2. ViewModel – this folder will contain several files:
    • sgs.model.common.js for reusable logic across all types of ViewModels
    • one file for the page-level view model – e.g. sgs.model.index.js
    • one file for each view-specific view model – e.g. sgs.model.savings-goal.js –
  3. ViewMediator – this folder will contain several files:
    • sgs.mediator.common.js for reusable logic across all view mediators
    • one file for the page-level view mediator – e.g. sgs.mediator.index.js
    • one file for each view-specific view mediator – e.g. sgs.mediator.savings-goal.js –

So at this point, create the folder structure described above as well as stub content for sgs.model.common.js and sgs.model.common.js (they can remain empty for now). And let’s update the LAB.js section to load these 2 files (before application.js):

<head>
	<!-- ... -->
	$LAB
		// ...
		.script("scripts/viewmodel/sgs.model.common.js")
		.script("scripts/viewmediator/sgs.mediator.common.js")
		.script("scripts/application.js").wait(function(){
			// When ALL scripts have been loaded AND executed:
			InitializeApplication();
		});
	</script>
	
	<!-- ... -->
</head>	

Here is a quick synopsis of the architecture for the first increment of our app:

3. Create the Savings Goal View

Now that we have a minimal shell in index.html,
let’s add a section tag to represent our first panel / view: the savings-goal-view.
The view will let us enter a savings goal amout and a maximum number of months.

<body>
	<section id='savings-goal-view'>
	</section>
</body>

Now we can add the two input elements (savings-goal-amount and savings-max-duration).
And we’ll have a span to display the calculated (derived) savings-target-per-month. This is what the view will look like:

Important Note: In this tutorial, instead of “inlining” the data-binding declaration with the view model inside the markup (like you will see in the KnockoutJS site or on any other simple tutorials), I am proposing a more “unobtrusive” approach consisting of keeping the markup pure and explicitly defining data-bindings separately later (in the view mediator which we’ll see soon in the next section).

<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" /><br/>

	<label for="savings-target-per-month">Savings Target Per Month:</label>
	<span id="savings-target-per-month" /><br/>
</section>

At some point in the future, once the number of views increases beyond a couple, we’ll externalize our views as individual [jQuery] template files – but we’ll see the approach in detail later.

Note: as we go along in the tutorial, I will not cover the CSS styling explicitly but you can always get the CSS when downloading the source based on the tag for a given step. Example: tag: part2-todo-2.zip (also available in tar.gz form). Look under for css/application.css

4. Create the Savings Goal ViewModel

If you have not yet created the ViewModels folder under the scripts folder, do that now. And then create a file named: sgs.model.savings-goal.js.

Here is a diagram summarizing what we’re about to implement:

As we indicated earlier, we’ll use namespaces to structure our code and avoid name collisions with other libraries.
There are various approaches for namespacing code in Javascript, some uses object literals, and some use functions as containers for our functions. Elijah Manor has a great summary highlighting the pros and cons of the various approaches.

For our needs and to make it easy to extend and edit our namespace we’ll use the object literals.
To lazy initialize our namespace hierarchy let’s add the following snippet, which checks if each node in our namespace exists and creates it if it is not yet defined.

// Lazy initialize our namespace context: sgs.model.savingsgoal
if (typeof(sgs) == 'undefined') sgs = { }
if (typeof(sgs.model) == 'undefined') sgs.model = { }
if (typeof(sgs.model.savingsgoal) == 'undefined') sgs.model.savingsgoal = { }

Now we can create functions prefixed by our namespace such as for e.g. the function to initialize the view model and its two value models:

sgs.model.savingsgoal.initializeViewModel = function (pageSettings) {
	// We can use properties of the pageSettings as default values for any of our ValueModels
	// If pageSettings are not provided we'll initialize an empty object
	if (typeof(pageSettings) == 'undefined') var pageSettings = { }
	
	var viewModel = {
		savingsGoalAmount: ko.observable(pageSettings.defaultSavingsGoal || 0), // dollars
		savingsMaxDuration: ko.observable(6), // months
	};
	
	return viewModel;
}

So our viewModel is basically an object literal (hash) where each property value is a value model,
implemented as KnockoutJS observable, essentially a function acting as an interceptor for a value. Everytime the function is acting as a setter, the interceptor notifies all observers (subscribers) of the value change.

Now let’s add a derived function (a.k.a. a dependent observable in KnockoutJS-speak) named savingsTargetPerMonth:

sgs.model.savingsgoal.initializeViewModel = function (pageSettings) {
	// We can use properties of the pageSettings as default values for any of our ValueModels
	// If pageSettings are not provided we'll initialize an empty object
	if (typeof(pageSettings) == 'undefined') var pageSettings = { }
	
	var viewModel = {
		savingsGoalAmount: ko.observable(pageSettings.defaultSavingsGoal || 0), // dollars
		savingsMaxDuration: ko.observable(6), // months
	};
	
	viewModel.savingsTargetPerMonth = ko.dependentObservable(function() {
		var result = 0;
		if (this.savingsMaxDuration() > 0) {
			result = this.savingsGoalAmount() / this.savingsMaxDuration();
		}
		return result;
	}, viewModel);

	return viewModel;
}

Note that ko.dependentObservable takes a function as a parameter as you would expect, but can also take a second optional parameter used to define the meaning of this inside the derived function. Typically we always pass the variable containing our viewModel.

Over time we can add more business logic to our sgs.mode.savingsgoal module.

5. Create the Savings Goal View Mediator

If you have not yet created the ViewMediators folder under the scripts folder, do that now. And then create a file named: sgs.mediator.savings-goal.js.
This is the module where we’ll place our data-binding logic as well as any code related to mediating access between the page, the savings goal view, and its view model.

Here is a diagram summarizing what we’re about to implement:

Let’s define our namespace:

// Lazy initialize our namespace context: sgs.mediator.savingsgoal
if (typeof(sgs) == 'undefined') sgs = { }
if (typeof(sgs.mediator) == 'undefined') sgs.mediator = { }
if (typeof(sgs.mediator.savingsgoal) == 'undefined') sgs.mediator.savingsgoal = { }

if (typeof(console) != 'undefined' && console) console.info("sgs.mediator.savingsgoal loading!");

Now as a convention, let’s create a new function for our mediator named createViewMediator
which we will eventually invoke from the InitializeApplication method in application.js.
The createViewMediator function will have 3 responsibilities:

  1. Instantiate a view model for the savings goal view
  2. Declare the data-binding between the HTML elements of the view and their corresponding value models in the view model
  3. Ask KnockoutJS to make the bindings effective (they will be live right after that)
  4. Save off the view model so we can access it from other parts of the app

So let’s implement the first responsibility to instantiate a view model for the savings goal view (using the initializeViewModel function we just created in the savings goal view model module).

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// Create the view Savings Goal view-specific view model
	var viewModel = sgs.model.savingsgoal.initializeViewModel(pageSettings);
}

Now let’s declare each of the 3 data-bindings we need:

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// Create the view Savings Goal view-specific view model
	var viewModel = sgs.model.savingsgoal.initializeViewModel(pageSettings);
	
	// 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: savingsTargetPerMonth()");
}

Now we’ll ask KnockoutJS to “apply”, i.e. register / enable our bindings.

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// Create the view Savings Goal view-specific view model
	var viewModel = sgs.model.savingsgoal.initializeViewModel(pageSettings);
	
	// 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: savingsTargetPerMonth()");
	
	// Ask KnockoutJS to data-bind the view model to the view
	var viewNode = $('#savings-goal-view')[0];
	ko.applyBindings(viewModel, viewNode);
}

This is where KnockoutJS is performing some heavy lifting behind the covers such as:

  • Setting observers on the actual HTML elements
  • Connecting these observers with the value models of the view model
  • Getting the initial value of each value model
  • Initializing each HTML element with that corresponding initial value

To make it possible to store and retrieve our view model later we’ll leverage the jQuery data API.
So let’s create 2 functions in the savings goal mediator (since it is acting as an intermediate between the browser, page, view and the model:

sgs.mediator.savingsgoal.getViewModel = function() {
	return $(document).data("sgs.model.savingsgoal.viewmodel");
}

sgs.mediator.savingsgoal.setViewModel = function(viewModel) {
	$(document).data("sgs.model.savingsgoal.viewmodel", viewModel);
}

Now we can use the setViewModel function inside createViewMediator to save off our freshly-created view model.

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// Create the view Savings Goal view-specific view model
	var viewModel = sgs.model.savingsgoal.initializeViewModel(pageSettings);
	
	// 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: savingsTargetPerMonth()");
	
	// Ask KnockoutJS to data-bind the view model to the view
	var viewNode = $('#savings-goal-view')[0];
	ko.applyBindings(viewModel, viewNode);

	// Save the view model
	sgs.mediator.savingsgoal.setViewModel(viewModel);	

	if (typeof(console) != 'undefined' && console) console.info("sgs.mediator.savingsgoal ready!");
}

6. Link the Page and the View Mediator

Here is a diagram summarizing how we’ll be linking our parts:

Ok now we’re at a point to start testing our first draft, we just need to add a call to the overall page InitializeApplication function (located in scripts/application.js) to our savings goal createViewMediator function like so:

function InitializeApplication() {
	if (typeof(console) != 'undefined' && console) 
		console.info("InitializeApplication starting ...");
		
	// Initialize our page-wide settings
	var pageSettings = { defaultSavingsGoal: 500 }
	
	// Create / launch our view mediator(s)
	sgs.mediator.savingsgoal.createViewMediator(pageSettings);

	if (typeof(console) != 'undefined' && console) 
		console.info("InitializeApplication done ...");	
}

So now you should be able to load your index.html page in Firefox with the Firebug console on and watch the debug statement and finally our view displays with our initial data as well as the initial derived data for the “Savings Target Per Month” when you tab out of the amount or max number of months input fields:

7. Apply Data Formatting / Masking Business Rules

So far we have a nice simplistic example but if you build a solid app you will want to format and ensure our amounts are correct / valid / displayed according to conventional rules.
To my knowledge (having researched these types of libraries for a while), only library from PengoWorks has currency masking capabilities and will format the amount as you type it.
Since that library stopped being maintained in 2007, I started updating it (after checking with the original author, David Switzer) to make it work with current browsers). The resulting library is called CurrencyMask JS.
So let’s download the latest version to our ./scripts/vendor folder, and add a code snippet in our <head> LAB section:

<head>
	<!-- ... -->
	$LAB
		// ...
		.script("scripts/vendor/currency-mask-0.5.0-min.js").wait()
		.script("scripts/application.js").wait(function(){
			// When ALL scripts have been loaded AND executed:
			InitializeApplication();
		});
	</script>
	
	<!-- ... -->
</head>	

To apply masking to a field, you instantiate a Mask object with the format you want (e.g. “$#,###” for amounts up to $9,999 without decimals) and you attach it to the targetted HTML element like so:

    var savingsGoalAmountMask = new Mask("$#,###", "number");
    savingsGoalAmountMask.attach($("#savings-goal-amount")[0]);

Let’s add the mask instantiation snippet to the savings goal view model (at the end of the initializeViewModel function in sgs.model.savings-goal.js). Also note that we will need to perform the initialization of savingsGoalAmount based on pageSettings later on in the mediator – so here we’ll set the default value of savingsGoalAmountFormatted to an empty string:

sgs.model.savingsgoal.initializeViewModel = function (pageSettings) {
	// ...

	var viewModel = {
		savingsGoalAmountFormatted: ko.observable(""), 
		savingsGoalAmountMask: new Mask("$#,###", "number"),
		// ...
	};
	
	// ...
}	

And let’s add the “attach” snippet to the savings goal view mediator in the createViewMediator function in sgs.mediator.savings-goal.js, right after the bindings declarations:

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// ...

	// Declare the HTML element-level data bindings
	// ...
	
	// Apply masking to the savings goal amount input field
    viewModel.savingsGoalAmountMask.attach($("#savings-goal-amount")[0]);

	// ...
}	

And if we refresh the page and start typing 1500 in the amount it should format as we type along and prevent any other character input, ensuring the amount is valid.

But … we just introduced an issue (see the infamous NAN displayed in the Savings Target Per Month)!
Well, once formatted the savingsGoalAmount value model will now be a string containing currency and thousands delimiter characters.
So let’s recognize that semantic change by re-naming our savingsGoalAmount value model as savingsGoalAmountFormatted value model:

sgs.model.savingsgoal.initializeViewModel = function (pageSettings) {
	// ...
	
	var viewModel = {
		savingsGoalAmountFormatted: ko.observable(pageSettings.defaultSavingsGoal || 0), // dollars
		// ...
	};

Now what we need to re-create a new savingsGoalAmount value model, and make it act as a two-way adapter to convert back and forth between formatted and unformatted values.
KnockoutJS can help us with that, using a “dependentObservable“. KnockoutJS also allows a hash to be passed to ko.dependentObservable. That hash can include the following key-value pairs:

  • owner – will specify the value of this
  • read – a function which can perform some post-processing and ultimately return the value of the observable
  • write – a function which can perform some pre-processing of a value and ultimately store it

So here is a skeleton of what the savingsGoalAmount value model would look like as a dependentObservable:

viewModel.savingsGoalAmount = ko.dependentObservable({
	owner: viewModel,
	read: function () {
		// some post processing code
	},
	write: function (value) {
		// some pre processing code
	}
});	

Let’s tackle the read function first. It will need to:

  • Get the current value model from savingsGoalAmountFormatted
  • Ask the savingsGoalAmountMask for a stripped (unformatted) value
  • Return a valid float value

Here is what the code looks like for the “read”:

	read: function () {
		// Get the current formatted value model 
		// Important Note: Even though we don't use the formatted_amt variable
		// in the rest of the function, we need to let KnockoutJS "know"
		// that this closure has a dependency on savingsGoalAmountFormatted
		// otherwise the closure will never be invoked on a read.
		var formatted_amt = this.savingsGoalAmountFormatted();

		// Unformat the value
		var amt = this.savingsGoalAmountMask.strippedValue;
		
		// Convert the result to a float
		if (amt.length == 0) { amt = 0 };
		return parseFloat(amt);
	},

Now let’s tackle the write function first. It will need to:

  • Ask the savingsGoalAmountMask to format the value
  • Set the savingsGoalAmountFormatted to the formatted value
  • Return the passed value

Here is what the code looks like for the “write”:

	write: function (value) {
		// Format the passed in value using the mask
		var formatted_value = this.savingsGoalAmountMask.updateFormattedValue(value);
		
		// Update the savingsGoalAmountFormatted value model
		this.savingsGoalAmountFormatted(formatted_value);
		return value;
	}

The complete and refactored savingsGoalAmount dependentObservable will now look like this:

viewModel.savingsGoalAmount = ko.dependentObservable({
	owner: viewModel,
	read: function () {
		// Get the current formatted value model
		var formatted_amt = this.savingsGoalAmountFormatted();
		
		// Unformat the value
		var amt = this.savingsGoalAmountMask.strippedValue;
		
		// Convert the result to a float
		if (amt.length == 0) { amt = 0 };
		return parseFloat(amt);
	},
	write: function (value) {
		// Format the passed in value using the mask
		var formatted_value = this.savingsGoalAmountMask.updateFormattedValue(value);
		
		// Update the savingsGoalAmountFormatted value model
		this.savingsGoalAmountFormatted(formatted_value);
		return value;
	}
});	

Now refreshing the index.html page should allow you to enter amounts and see the calculated value!

Here is a diagram summarizing how we restructured the various parts using the dependent observables and the mask:

I will leave the exercise for you dear reader to follow the same pattern for the “Savings Max Duration (In Months)” input field.
In that case the mask will be a little simpler since we need at most 2 digits.

8. Apply Custom Masking Rules

For custom masking needs, such as for example a phone number, social security id, special identifier,
I strongly recommend Josh BushMasked Input Plugin.

For dates I suggest using Masked Input together with jQuery Datepicker since the calendar makes it easy to select a date but does not provide masking features.

9. Applying Formatting Rules

When using our simple view you might have noticed that the “Savings Target Per Month” may show some numbers with many decimals. This is because the <span> element is data-bound to the savingsTargetPerMonth dependentObservable which returns a float. So we have two options:

  • Apply formatting to the float inside savingsTargetPerMonth
  • Or create another dependentObservable named savingsTargetPerMonthFormatted, in which we can apply the needed formatting logic. Then we would change the data binding for the <span> to use our new value model.

The cleanest approach is #2 since it also allows you in the future to build other dependentObservables on top of savingsTargetPerMonth. But if you don’t think you will need that ability then #1 will work and is very simple.

Approach 1 would require us to format the calculated value using the formatMoney function of Accounting.js like so:

		var result = this.savingsGoalAmount() / this.savingsMaxDuration();
		var formattedResult = accounting.formatMoney(result, "$", 2, ",", ".");  ;
		return formattedResult;

Approach 2 would require us to add a new dependentObservable named savingsTargetPerMonthFormatted in our savings goal view model:

sgs.model.savingsgoal.initializeViewModel = function (pageSettings) {
	// ...
	
	viewModel.savingsTargetPerMonthFormatted = ko.dependentObservable(function() {
		var result = 0;
		if (this.savingsMaxDuration() > 0) {
			result = this.savingsGoalAmount() / this.savingsMaxDuration();
		}
		var formattedResult = accounting.formatMoney(result, "$", 2, ",", ".");  ;
		return formattedResult;
	}, viewModel);
}

And we then need to update the data-binding code in the createViewMediator function of our mediator module:

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// ...
	
	$("#savings-target-per-month").attr("data-bind","text: savingsTargetPerMonthFormatted()");
	
	// ...
}

Let’s refresh the index.html page and now when we enter $500 and 7 months, the max per month updates to dollar formatted two-decimal amount: $71.43!

10. Applying Visual Feedback Cues

When web pages require a round-trip to the server, the user waits and then sees the whole page refreshing, but in rich interactive apps since processing happens often very fast without a page refresh, we need to provide visual cues when changes occur. For quick feedback logic (e.g. not requiring an Ajax call or processing longer than a second) I recommend using a jQuery UI “effect”, such a temporary highlight using a soft color, like so:

		$("#savings-target-per-month")
			.effect('highlight', { color: 'LightGreen' }, 3000); // for 3 seconds

Logically the code belongs to the mediator, so let’s subscribe to the savingsTargetPerMonthFormatted value model changes
inside our createViewMediator function.

sgs.mediator.savingsgoal.createViewMediator = function (pageSettings) {
	// ...

	// Subscribe to interesting value model changes
	viewModel.savingsTargetPerMonthFormatted.subscribe(function() {
		$("#savings-target-per-month")
			.effect('highlight', { color: 'LightGreen' }, 3000); // for 3 seconds
	});
	
	// ...
}

Let’s refresh index.html, and whenever we see the xxx recalculate, the light green highlight of the result should fade in and out during our 3-second timeframe!


Overall Recap

So at this point we have built a reasonably solid foundation for our app even though it contains only one view. The architectural steps we took to decouple view, viewmodel, view mediator and the page will yield some benefits once we add continue adding features to our app (in part 3).
So here is a quick recap of the steps we followed:

# To Do Area (Module)
1 Create a Shell Application Main app
2 Organize your Application Code Main App
3 Break down each independent panel into Views View
4 Create a ViewModel for each View View Model
5 Create a View Mediator for each View View Mediator
6 Link the Page and the View Mediator(s) Main App
7 Apply Data Formatting / Masking Business Rules View
8 Apply Custom Masking Rules View Mediator
9 Applying Formatting Rules View Mediator
10 Providing Visual Feedback Cues View Mediator

So What?

Many KnockoutJS tutorials focus on giving you the basics to run simple scenarios. My approach here although more “architected” is geared at building rich interactive apps made of multiple panels/views with solid interactions.

The modularization of our code base will also lend itself better to unit testing and even BDD testing using Javascript test frameworks like Jasmine and Jasmine-Species. They will allow us to write tests against our view models and to a certain degree some of the various Javascript modules.

We have focused on a one-view increment for the app but in Part 3 we will cover the following topics:

  • Adding more views, viewmodels and mediators to our basic app
  • Sharing data across views and mediators
  • Implementing masking and formatting
  • Incorporating rudimentary elements of usability
  • … and more!

So stay tuned!

References and Resources

Patterns
Frameworks And Blogs

Javascript Loaders

jQuery Plugins

Other Javascript Libraries

Javascript Test Frameworks
My Other Related Posts:

Full Source Of The Savings Goal Simulator (KnockoutJS Demo)

The whole application is available on Github under techarch / savings-goal-simulator.

Credits

Special thanks for Thibaut Barrère and Susan Potter for their feedback while I was working on drafts for this tutorial! :-)

If you enjoyed this post, I would love it if you could check out mySkillsMap, my skills management app.

October 17th, 2011 Posted by | Ajax, javascript, Patterns, rich web apps, Tools | 14 comments