KnockoutJS

Resources

·         http://learn.knockoutjs.com (interactive tutorial site)

·         http://blog.stevensanderson.com/2010/07/12/editing-a-variable-length-list-knockout-style/

 

Key Selling Points

·         MVVM pattern

·         Data-binding. Zero to many subscribers (typically UI elements) are attached to 'observable' values (typically properties on the ViewModel). When a value is changed (through bound UI element or the accessor wrappers on the value) all bound items are notified. This includes calculated fields, showing/hiding elements ('This order exceeded your approved spending limit'), custom handlers, etc.

·         Templating. In particular, arrays can be bound to templates, making (for example) adding and removing rows from the UI simply a case of adding or removing a row from the observed array.

·         UI feels fast as a site implementing KnockoutJS to its best advantage (a) performs far fewer postbacks to the server than the equivalent WebForm or MVC application, and (b) performs far fewer DOM updates when refreshing complex UI than a typical web application does.

 

Scripts & Licensing

·         Scripts: (~25k)

·         As of (8/16/2011) the version of jQuery used by Knockoutjs.com is 1.6.1

MVVM Pattern

The Model-View-ViewModel pattern differs from the traditional MVC pattern in that it virtually eliminates all 'code-behind' from the View layer. It has its roots in WPF & Silverlight.

·         Model – The DTOs or Data Layer

·         View – UI

·         Controller – Code logic. Optional (some people see it as part of the MVVM model, others don't)

·         ViewModel – Data binding logic (converts model information into view information). In the traditional MVC pattern, this code would have lived in the Controller.

For simple UI, MVVM is considered overkill even by its creators.

ViewModel

In KnockoutJS the ViewModel is entirely defined in JavaScript:

// Real world views are more complex, but this is a completely valid ViewModel

var myModel = { name:"Dave", occupation: "Astronaut",

         helloWorld: function() {alert(this.name() + " says hello"); };
ko.applyBindings(myModel);

Observables

Observables are items (typically ViewModel properties) that automatically issue notification messages when their value changes. Observables are at the heart of Knockoutjs.com.

var myModel = { name:ko.observable("Dave"), occupation: ko.observable("Astronaut") };
ko.applyBindings(myModel);

Once marked observable, all data-bind items attached to the observable item will refresh when the value changes. This includes computed values (see below).

Observable are accessed through their get (this.name()) and set (this.name("John"))  accessors rather than directly (presumably so the framework has somewhere to get its hooks into). The set accessors follow the fluid API patent and return the parent (so this.name("John").age(30).location("Wormwood Scrubs"); is valid.

Variable and Property Observables

These are the most common observables and use the ko.observable(value) syntax show above. Arrays require a slightly different syntax to (ko.observableArray(array))

Dependent Observables

Functions can also be observed.

myModel.greeting = ko.dependentObservable(function() {

         return this.name() + " the " + this.occupation() + " says hello"; },

         myModel)

KnockoutJS is smart enough that changing name or occupation will cause any UI bound to greeting to be updated!

?? How on earth does it know to do this? Is it parsing the function, or simply recording the dependencies on first execution / initial display of the page? What if the path through the code first time doesn't has as many dependencies as it does on the second and subsequent passed ??

?? A simple test showed that a function declared in the myModel that had dependencies required no wrapping and was called anyway. This may be a bug. Repro[i] ??

 

Data Binding

The data-bind Attribute

ViewModel properties and method are UI bound using the data-bind attribute. This name 'data-bind' should be interpreted in the widest possible sense, as the binding can link expression to other aspects of the UI that just its value. These include assigning button click handlers, UI visibility, etc.

Some examples:

<p>Name: <span data-bind="text: name"></span></p>
<p>Occupation: <span input data-bind="value: occupation" /></p>
<p>Total:<span data-bind="text: bookings().length"></span></p>
<button data-bind="click: helloWorld">Hello</button>
<select data-bind="options: flights, value: flight, optionsText: 'name'" />

ViewModel value-bound fields are automatically updated, but since the ViewModel is not the Model these changes are not permanent (in the sense they will be lost if the page is reloaded).

Enable, Visible

The data-binding attribute has an optional enable and visible clauses. These work as you'd expect:

<button data-bind="click: addRow, enable: bookings().length < 5">Add Row</button>

Css

The data-binding attribute has an optional css clause:

?? TODO ??

 

 

Drop-down lists

Note that for
  <select data-bind="options: flights, value: flight, optionsText: 'name'" />
  <select data-bind="options: [1,2,3,4,5], value: rating" />

·         options
The object collection (array) containing the objects to select from. This has the odd requirement (presumably due to a scoping / navagation challenge) that the array must be defined on each member of the array. See the flight entity and flights array in the 'delete booking' example later for an example.

·         value
The name of the ViewModel property that holds and is bound to the current selected item from the options array.

·         optionsText [optional]
The name of the property that is on each item in the options array that holds the display text.

Computed Values

Computed values are allowed and can be bound, but need to be defined in the ViewModel. They must be defined as a dependentObservable and set before applyBindings is called.

Using non-observable properties and a straight function in didn't work in the provided UI. Not tried in actual code:

// DOESN'T WORK! :-(

// (and actually displays ' function() { return this.name() + " is a " + this.occupation(); }'!)

var myModel = { name:"Dave", occupation: "Astronaut" };

myModel.summaryText = function() { return this.name() + " is a " + this.occupation(); };
ko.applyBindings(myModel);

 

// Works! :-)
var myModel = { name:ko.observable("Dave"), occupation: ko.observable("Astronaut") };

myModel.summaryText = ko.dependentObservable(function() {

         return this.name() + " is a " + this.occupation(); }, myModel);
ko.applyBindings(myModel);

?? What happens if I try to define an circular dependency ??

 

Navigation

KnockoutJS encourages the use of hash navigation (where the only part of the URL that changes is the part after the # anchor mark). For example, instead of "http://myDomain/myApp/ViewProduct?id=123" use "http://myDomain/myApp#page=ViewProduct&id=123".

Such URLs are still bookmarkable and changes to just the # can be made without trigging trips to the server. In effect, the navigational position is now a property of the ViewModel.

Parameters on the URL can be linked to ViewModel properties via ko.linkObservableToUrl:

ko.linkObservableToUrl(property, "parameterName" [, defaultValue])

Calls to ko.linkObserableToUrl are in addition to existing ko.linkObservable bindings.

The knockout.address.js script needs to be included for this to work.

 

Templates

KnockoutJS templates are HTML generated from an JavaScript object graph. The default template engine for KnockoutJS is jQuery Templates.

?? TODO work out how much of this is KnockoutJS functionality and how much is jQuery Templates ??

Array Part

Firstly, we need data to run the template off. So we create an observableArray.

// Base data for this example (will be unchanged by our UI)

var flights = [ {name:"BA49", price:1500}, {name:"CAN54", price:1000} ];

var people = [ {name:"Dave"}, {name:"John"} ];

 

// Constructor for our entity (returns a booking reference entity)

var makeBooking = function(bookingReference, peopleIndex, flightIndex) {

  this.bookingReference = bookingReference; // Not observable (because I say so)

  this.person = ko.observable(people[peopleIndex]);

  this.flight = ko.observable(flights[flightIndex]);

 

  // Knockoutjs requires the source data for the drop down to be available

  // from the entity

  this.flights = flights;

 

  // Since we want to delete a specific line,we define the delete hook at the entity level

  this.deleteMe = function() { viewModel.bookings.remove(this); };

};

 

var viewModel = {

         bookings: ko.observableArray([

                          new makeBooking("DaveBA", 0, 0),

                          new makeBooking("JohnCA", 1, 1) ]),

 

     // Add a method to the view model for adding new rows to the array

         addRow: function() { this.bookings.push(new makeBooking("", 0, 0)) }

};

ko.applyBindings(viewModel);

 

Note that since the array is bound, the UI should automatically add and delete rows as we add and delete items from the array.

 

Template Part

<table>

  <thead><tr><th>Booking Reference</th><th>Passenger Name</th><th>Flight Number</th></tr></thead>

  <tbody data-bind="template: {name:'bookingTemplate', foreach: bookings}"></tbody>

</table>

<p>Total bookings:<span data-bind="text: bookings().length"></span></p>

<button data-bind="click: addRow, enable: bookings().length < 5">Add Row</button>

 

...

 

<script type="text/x-jquery-tmpl" id='bookingTemplate'>

  <tr>

    <td>${bookingReference}</td>

    <td>${person().name}</td>

    <td><select data-bind="options: flights, value: flight, optionsText: 'name'" /></td>

 

    <!-- Attach a call to the deleteMe method defined on the entity instance -->

    <td><a href="#" data-bind="click: deleteMe">Delete</a></td>

  </tr>

</script>

Note how the template binds to a property of the ViewModel by name magic only.

 

Template Buttons, Links etc

These work as you'd hope (i.e. are automatically scoped by the template to the correct instance). Eg.

<!-- Link to deleteMe on the entity instance used to create the row -->

<a href="#" data-bind="click: deleteMe">Delete</a>

 

<!—- Button to do the same thing -->

<button data-bind="click: deleteMe">Delete</button>

 

        

Template Tricks & Tips

·         A ${new Date}  element in the template can show how often the template is being updated (i.e. how much churn our observable wrapper triggers)

 

Custom Bindings

Binding and update events can be hook into by

1.       Specifying a new property:accessor section in the data-bind line. The accessor is any valid expression, ViewModel reference, or constant and is passed in for evaluation by the event handler. This allows for generic handlers (such as fade in, hide, or highlight) as the trigger logic can be moved to the ViewModel, data-binding, or local method.

2.       Adding a property of the same name ko.bindingHandlers. The property holds an object containing the (optional) event handlers to be called when the binding is first initialized (init) or updated (update).

Both handlers take up to two parameters:
  {init: function(element, accessor), update: function(element, accessor)}
where element is the HTML element bound to and accessor is the callable accessor which runs and returns whatever logic you assigned to it.

For example:

<p data-bind="bookingCheck: bookings().length">Maximum number of bookings reached.<p>

...

ko.bindingHandlers.bookingCheck = {

  update: function(element, accessor) {

         (accessor() >=5) ? $(element).fadeIn() : $(element).fadeOut();

  }

}

Or more generically:

<p data-bind="fadeInOut: bookings().length > 5">Maximum number of bookings reached.<p>

...

ko.bindingHandlers.fadeInOut = {

  update: function(element, accessor) {

         accessor() ? $(element).fadeIn() : $(element).fadeOut();

  }

}

Note that the init handler appears to often be used for far more than data-binding initialization. For example, adding a "jqButton: true" to the data-binding and ko.bindingHandlers.jqButton {init: function(element) {$(element).button());}}} would turn that button () into a jQuery UI style button().

Observables

Observables can also be directly subscribed to:

myViewModel.bookingReference.subscribe(function(newValue) {...});

 

Persisting Data To The Server

Traditional Form Postback

If the values to be preserved are already in form fields, a normal submit button is enough. However, if the data is in (for example) an array property on the ViewModel, the data would first need to be copied to a hidden field. KnockoutJS's data-binding and ko.toJSON() features can be of help here (at the cost of often serializing unnecessarily).

Example:

<form action="/myPage" method="post">

  <input type="hidden" name="myData" data-bind="value: ko.toJSON(propertyName)" />

  <button type="submit">Save</button>

</form>

 

JSON

ko.utils.postJson(location.href, {data});

Scoping

ko.applyBindings(viewModel, element) applies viewModel to that DOM element (and its descendants) only. This allows for multiple viewModels to be specified in the same overall DOM.

Thoughts

Pros

·         Linked UI updates / notifications

·         Efficient updates for scenarios based around small changes to large data / UI sets

·         Templates integration was natural and appears efficient

·         UI navigation feels fast as there are very few postbacks compared to typical WebForms or MVC site.

Cons

·         Hassle of keeping the Model and ViewModel in sync.

·         Uses the 'big-ball-of-mud" anti-pattern

·         Inefficient if changes are large or non-detectable? (e.g. if we are downloading the entire spreadsheet each time rather than changing a couple of cells)

·         The performance gains of 'single page applications' requires all the logical pages to exist in a single file. This has numerous downsides:

o   Changes cannot be made in isolation

o   Difficult to work with in a team environment and/or under source control

·         No clear test strategy

·         Requires specific version of jQuery? (1.6.1 at time of writing)

 

Unknowns

·         What does the default JSON for a ViewModel instance look like? Is it clean enough to use as a DTO to the server?

·         JQuery compatibility

·         Support / adoption story

·         If I write a new UI control can I wire it up to notifications?

·         Can I support more than one root in the MVVM at the same time (e.g. User detail and Form detail)?

·         Knockout.js encourages you to embed all your UI into a single physical page and simply hide all but the part you want to show to the user at that time. It is unclear to me how much (if any) data-bound updates are issued to the hidden elements.

 

Conclusions

Cons

 



[i] Bug Repro:

Based on the documentation, the field bound to fullName() and currentDate() should not update when the user changes the value in the lastName text box. When run (8/17/2001) the fullName() updates (currentDate() acts as expected).

<p>Last name: <span data-bind="text: lastName"></span></p>
<p>Date: <span data-bind="text: currentDate()"></span></p>
<p>Fullname: <span data-bind="text: fullName()"></span></p>
<p>Last name: <input data-bind="value: lastName" /></p>

var viewModel = {
  lastName: ko.observable("Bertington"),
  fullName: function() {return this.lastName() + " " + new Date;},
  currentDate: function() {return new Date;}
};
ko.applyBindings(viewModel);