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:
- 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
- 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
- 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
- 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.
- 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. - 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:
- 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.
- 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); }
- Derived attributes, resulting from a calculation, or a business rule.
- 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);
- 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); });
- The ViewModel can also aggregate a subset of attributes from one or even several server-side [MVC] Models
- 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
- Model View – ViewModel Pattern
- MVC Xerox Parc 1978-79
- MVC Pattern
- Observer Pattern
- Publish Subscribe Pattern
- Value Model Pattern
- Data Binding Pattern
Frameworks And Blogs
- KnockoutJS
- Learn KnockoutJS (Interactive Tutorial / Playground)
- jQuery
- jQuery UI
- jQuery Template
- jQuery Template API
- Knock Me Out (Steve Sanderson’s Blog)
- Knock Me Out (Ryan Niemeyer’s Blog)
- Patterns For Large-Scale Javascript Architecture (Addy Osmani)
Source Of The KnockoutJS Examples
You can find the examples on jsFiddle.net here: