The "Tech. Arch."

Architecting Forward ::>

Creating Rich Interactive Web Apps With KnockoutJS – Part 1

Intro

With the advent of online games, social apps, Flash/AIR apps, and rich instant-feedback sites
like Google Search and GMail, consumers are demanding more and more interactivity.

While interacting with your app customer now expect:

  • Browser-side processing (calculation, business rules, …)
  • Instant validation
  • Instant visual feedback: animation/effect while making changes (add/remove/update)
  • Dynamic clues on what to do next
  • More same / on page functionality (without the need to refresh or navigate to other pages)

For a while adding a sprinkling of AJAX and localized visual refresh using jQuery UI was sufficient.
But richer application interactions introduce more elaborate technical approaches:

  • Present more related data at the same time
  • Leverage graphs and other data visualization techniques
  • Offer more on-page functionality
  • Allow interactions with one panel to cause updates on other panels
  • Support asynchronous back-end interactions

Extending the Model View Controller Pattern

Here is a reminder of what typically happens in a traditional MVC web app (e.g. Ruby On Rails, Ruby Camping, ASP.NET MVC, Cake PHP, Django Python, etc.):

  • The routing engine identifies the appropriate controller
  • The controller retrieves the model
  • The view engine renders the view
  • The view contains HTML, CSS, and Javascript code
  • Rich browser-side applications typically have event handlers to allow page-level interactions to
    handle validation, hide/show elements, perform some processing / calculations, executebusiness rules, invoke AJAX services, update content

Note that the last bullet represents quite a bit of logic (possibly in the 500 to several thousand lines of Javascript code).
The logic gets even more complex once you have multiple panels and sets of model elements.
If your AJAX services return complex objects you also need to keep track of these objects,
their binding to panel elements/controls, and their inter-dependencies / impact on each other when a change occurs.

Key take-away:

We need to organize our browser-side code (markup, CSS, and JS) according to their concern, i.e. model representation, business rules, display, user interaction, event handling, server interaction, etc..

This is where the notion of ViewModel and Observables come in to help us out.
Let’s break things down:

  1. The ViewModel is a specialized browser-side Javascript model representing a group of data elements a given page or panel (View) will display or facilitate interaction with.



    Benefits:

    • Partitioning: each page / page can have its own clean model
    • Testability: the View and ViewModel can be tested independently from the server logic
    • Modularity: ViewModels code can be namespaced and contained in their own .js file


  2. Each attribute of a ViewModel will be mapped to an Observable so that any value change can be broadcasted (a.k.a. “published” in the publish/subscribe terminology) to any interested subscriber. (So essentially a ViewModel attribute is what is called a Value Model)



    Benefits:

    • Fast UI: derived values or content can be produced on-the-fly based on observed attributes
    • Increased responsiveness: asynchronous server interactions can be triggered in the background and cause ViewModels to be updated


  3. The scenario where a UI element subscribe to a ViewModel attribute (Value Model) is known as “Data-Binding“.
    This allows a clean separation between logic related to the ViewModel (calculations, derivations, retrieval, server interaction) and UI-specific presentation logic.



    Benefits:

    • Synchronization: HTML elements and ViewModel attributes are kept in synch
    • Instant visual feedback: elements on the page can update visually as you interact / update data


Introducing KnockOutJS

KnockOutJS is a Javascript framework created by Steve Sanderson to provide support for ViewModel, Observable, and Data-Binding using jQuery.
For more details, check out the KnockOutJS site as well as the Learning Playground.

Let’s dig into the basic infrastructure provided by KnockOutJS and then we’ll build up to more complex scenarios.

Basics

  1. In KnockoutJS, a ViewModel is defined as a Javascript Object Literal. Value Models are defined using ko.observable() and an initial value as follows:
    var viewModel = {
    	savingGoalAmount: ko.observable(500), // dollars
    	// ... more attributes ...
    };
    

    The example ViewModel has one attribute: savingGoalAmount, with an initial value of 500.

  2. You can programmatically “subscribe” to changes in a ViewModel attribute (a Value Model) like this:
    // Set up the ViewModel
    var viewModel = {
    	savingGoalAmount: ko.observable(500), // dollars
    	// ... more attributes ...
    };
    
    viewModel.savingGoalAmount.subscribe(function(newValue) {
    	// Show the updated value on the FireBug's console
    	console.info("savingGoalAmount is now " + newValue);
    });
    
    // Get KnockoutJS to apply bindings / subscriptions
    ko.applyBindings(viewModel);	
    
    // Test by changing the amount
    viewModel.savingGoalAmount(600); // Should display in the console
    

    The subscribe function takes a callback function it will invoke upon any change on the value of the savingGoalAmount Value Model.

    The ko.applyBindings function tells KnockoutJS to start the dependency tracking. From that point on all subscribers will be notified of any changes.

  3. KnockoutJS makes it easy to setup data binding between any HTML element (e.g. INPUT) and a ViewModel attribute (Value Model) using the data-bind syntax and the value: keyword
    	
    

    KnockoutJS uses the data-bind attribute to let us specify the type of binding needed.
    In this example we’re binding the value of the INPUT element to the savingGoalAmount Value Model.
    We’ll see later on that there are other types of data-binding such as specific attributes, visibility, styling, etc. although you can see the full reference in the documentation.



    Note that the data-bind syntax is just “syntactic sugar” for use in our markup. If you prefer a clean separation between markup and Javascript, you can declare your data-bindings like so:

    $("#saving-goal-amount")
    	.attr("data-bind","value: savingGoalAmount");
    

    You just need to make sure these declarations are performed before you get KnockoutJS to apply the bindings (using ko.applyBindings).

You can play and run the examples above on this jsFiddle.net page.



Building Up

Now that we have a basic understanding of KnockoutJS, let’s add a few more building blocks:

  1. If a ViewModel attribute needs to store an array, KnockoutJS provides a specialized type of Observable called an ko.observableArray:
    var viewModel = {
    	savingGoalObjectives: ko.observableArray([ 'Discretionary', 'Expensive Purchase', 'Investment']),
    	// ... more attributes ...
    };
    

    Adding / removing items in the array will cause KnockoutJS to trigger an update to all observers of this Value Model.

  2. In addition to attributes (Value Models), ViewModels can expose “behaviors” such as:
    • Derived attributes, resulting from a calculation, or a business rule.

      KnockoutJS refers to them as dependentObservables:

      var viewModel = {
      	savingGoalAmount: ko.observable(500), // dollars
      	savingMaxDuration: ko.observable(6), // dollars
      	// ... more attributes ...
      };
      
      // Derived data
      viewModel.savingTargetPerMonth = ko.dependentObservable(function() {
      	return this.savingGoalAmount() / this.savingMaxDuration();
      }, viewModel)
      

      Whenever either savingGoalAmount or savingMaxDuration change, the derived savingTargetPerMonth function will be re-evaluated.
      If savingTargetPerMonth is data-bound to the UI, the corresponding element will redisplay.

      Note that you can create dependentObservable functions on top of dependent observables or plain observables!

    • Formatted version of the data
      var viewModel = {
      	savingGoalAmount: ko.observable(500), // dollars
      	// ... more attributes ...
      };
      
      // Derived formatted display data
      viewModel.savingGoalAmountAsCurrency = ko.dependentObservable(function() {
      	return accounting.formatMoney(this.savingGoalAmount(), 
      			"$", 2, ",", ".");  ;
      }, viewModel)
      

      Above is an example using the Accounting.js library to format our savingGoalAmount Value Model within our dependent observable so that we can display a nicely formatted currency representation on the UI.

    • Add / remove logic on arrays
      var viewModel = {
      	myItems: ko.observableArray(),
      	// ... more attributes ...
      };
      
      // Add / remove behaviors 
      viewModel.addItem = function (newItem) {
      	var valid = validateItem(newItem); // custom logic
      	if (valid) {
      		this.myItems.push(newItem);
      	}
      }
      

      In this example, addItem is a custom behavior attached to the ViewModel which can be invoked to add new valid items.

    • Any other business logic related to an overall ViewModel
      var viewModel = {
      	savingGoalAmount: ko.observable(500), // dollars
      	// ... more attributes ...
      };
      
      viewModel.reset = function() {
      	this.savingGoalAmount(500);
      }
      
  3. To allow other parts of our browser-side logic to access our ViewModels, we need to have a way to access them.

    We can save ViewModels in the DOM, either at the document level or at a panel level using the jQuery.data API:

    var viewModel1 = { }
    var viewModel2 = { }
    
    ko.applyBindings(viewModel1);	
    ko.applyBindings(viewModel2);	
    
    $(document).data("MyDocumentViewModel", viewModel1);
    $("#my-panel").data("MyPanelViewModel", viewModel2);
    

    This allows us to access a specific ViewModel later, outside data-binding logic (e.g. in an event handler) like so:

    var viewModel1 = $(document).data("MyDocumentViewModel");
    var viewModel2 = $("#my-panel").data("MyPanelViewModel");
    

    To keep our code organized we’ll want to create accessor functions such as for example:

    	MyApp.Model.GetMyDocumentViewModel = function() {
    		return $(document).data("MyDocumentViewModel");
    	}
    
    	MyApp.Model.SetMyDocumentViewModel = function(viewModel) {
    		$(document).data("MyDocumentViewModel", viewModel);
    	}
    

    So that we can just access our ViewModel from other modules:

    // Initialization
    var viewModel = { }
    ko.applyBindings(viewModel);	
    MyApp.Model.SetMyDocumentViewModel(viewModel);
    
    // Later on in a different module:
    MyApp.Model.GetMyDocumentViewModel()
    	.savingGoalAmount.subscribe(myCallBackFunction);
    
    


  4. The attributes (Value Models) of the ViewModel can be updated using data returned by an AJAX call to a controller
    $.getJSON('ajax/myapi.json', 
    			parms, 
    			function(data) {
    	// Assumption: we have saved the original viewModel 
    	var viewModel = $(document).data("MyViewModel");
    	viewModel.savingGoalAmount(data.amount);
    });
    
  5. The ViewModel can also aggregate a subset of attributes from one or even several server-side [MVC] Models

  6. Arbitrary Javascript functions can also subscribe to Value Model changes:
    // Set up the ViewModel
    var viewModel = {
    	savingGoalAmount: ko.observable(500), // dollars
    	// ... more attributes ...
    };
    
    function GraphSavingScenario(newValue) {
    	// some graphing logic
    }
    
    viewModel.savingGoalAmount.subscribe(GraphSavingScenario);
    


So What?

KnockoutJS can help organize browser-side logic by facilitating the creation of panels (Views),
each logically data-bound to ViewModels built on top of a local publish/subscribe mechanism using Observables.

Data-binding, combined with derived data / behaviors, and AJAX will boost the interactivity level of the application.

Part 2 of the series on “Creating Rich Interactive Web Apps” will be a tutorial on how to build a moderate complexity application with KnockoutJS.
We will cover the following topics:

  • Enabling / disabling rules
  • Condition display
  • Leveraging jQuery templates
  • Extracting views into templates
  • … and more!

So stay tuned!

References and Resources

Patterns
Frameworks And Blogs

Posted by | Ajax, javascript, jQuery | 10 comments

10 Comments

  1. I am a huge fan of all the work that you put into this post. I hope that you definitely publish the second part shortly. I linked to your post on twitter @ryankeeter, under the link http://ryankeeter.com/42 .

    What I like about your post is that you went a step further in defining what is commonly not recognized in many organizations: the visual refresh of jQuery UI and like frameworks is nice, but what happens when you start defining intricate models on the client side? When you start pushing your weight around on the client-side, you start running into issues that are very common in OO development.

    I like your site, I like this post, I am pumped about the time that you put into this content.
    My recent post DIY Photography: Build Your Own Softbox for $30

    Comment by Ryan Keeter | September 17, 2011

  2. That little aside about declaring data-bindings by appending an attribute has addressed the only issue I had using Knockout, so thanks! Great post.

    Comment by Dave Everitt | September 18, 2011

  3. Effing awesomeness ! Great work.

    Comment by FourFath3r | September 22, 2011

  4. Thank you soooo much !!!

    I'm currently trying to put all the pieces together with client-side coding, and was looking into knockout, so your post was spot on !

    Comment by @mrlucmorin | September 26, 2011

  5. Thanks for the excellent article. It had a few very helpful nuggets! Looking forward to part 2.

    Thanks!

    Comment by dbows | September 28, 2011

  6. Great article! What do you use for diagrams?
    My recent post RDRubyTutorial: ??????????????? Ruby

    Comment by Vladimir | October 21, 2011

  7. I have tried a lot of diagramming tools but the one I tend to use the most is actually … Powerpoint 2007 as the drawing capabilities (and connectors) are pretty powerful, and also because you can save a given slide in .png format. I then trim / tweak the image with Photoshop.

    Comment by techarch | October 22, 2011

  8. this article is the best I have ever read about architecture design with mvc & knockoutjs

    Comment by Martin | January 30, 2012

  9. I've had some issues with this pattern…

    While view models and observables are very good at keeping your data and UI in sync, I never know how to handle UI behaviors.

    Because elements can be created dynamically (i.e. through a foreach binding), you can't easily handle user interactions with them unless you do it through ko bindings and dom manipulation functions in the view model.

    Doing it that way gets really complicated when an object in your view model can be displayed in multiple places at once. A sidebar item can be clicked to show a more detailed view in a center column, for example. Now you have two completely disparate UI elements being controlled by the same view model.

    A "details" view might be handles as a drawer in your center column, and a tooltip in your sidebar. You can't just have a showingDetails() observable that controls the visibility of the child details element or when you open the drawer in the center column the tooltip in the sidebar pops up too! So you have to have two observables.

    Add more use cases, making device or resolution dependent elements, and your view model starts getting ridiculously complicated.

    Add to that the MVC instinct of wanting to keep your data handling separate from your presentation logic and you really start hating life…

    I use knockout a lot these days and can't live without the data binding, but I still haven't found an elegant way to handle UI behaviors with it…

    Anybody have any ideas or want to share how they're doing it?

    Comment by Nick | March 11, 2012

  10. great job

    Comment by @JackNova | August 8, 2013

Sorry, the comment form is closed at this time.