Knockout Tutorial: Build Dynamic, Interactive Web Pages Easily

Learn to use the powerful Knockout JavaScript library to quickly create interactive user interfaces for your websites and web apps. Lots of example code is included in the tutorial.

Knockout Tutorial: Build Dynamic, Interactive Web Pages Easily

JavaScript is a great language, but building complex user interfaces for websites and web apps using JavaScript alone can get tedious and fiddly. You have to deal with cross-browser issues, as well as write long-winded functions to manipulate the DOM (document object model), handle events, and more.

For this reason, a large number of useful JavaScript libraries have emerged in recent years to make your job as a JavaScript coder easier and more fun.

One recent library that's growing in popularity is Knockout. It's small, easy to use, and plays nicely with other JavaScript libraries such as jQuery. Using Knockout, you can build complex user interfaces in a fraction of the time that it would take you to code everything by hand.

In this tutorial, you learn Knockout from the ground up. You explore the key concepts behind Knockout, and learn how to create interactive web pages quickly and easily. At the end of the tutorial, you work through a complete "product selection" example that shows how to use Knockout in practice.

Press the View Demo button above to view the complete "product selection" example. You can also press the Download Code button to download all the code examples from this tutorial.

By the time you've finished reading, you'll be able to build rich, interactive web pages using Knockout, and you'll be well on the way to becoming a Knockout expert!

Let's kick things off with a brief look at Knockout's philosophy, features, and benefits.

What is Knockout?

Knockout is a small JavaScript library that makes it really easy to create interactive user interfaces for your websites and web apps. It's based on a coding pattern called Model-View-Viewmodel (or MVVM for short).

Essentially, Knockout works like this:

  1. You create your HTML view; that is, your user interface, or web page, containing text messages, text fields, checkboxes, select menus, and so on.
  2. You create a JavaScript view model object that holds all the data that appears in your view, and link the view model with the view using bindings.
  3. Then, any changes you make to the data in the view model object automatically update the widgets in the view.
  4. Conversely, if the user changes something in the view — such as selecting a checkbox — then the data in the view model updates automatically. (This, in turn, can cause other widgets in the page to update automatically.)

The great thing is that, once you've created your view and view model, Knockout updates them automatically, without you having to write any tedious JavaScript code to access and manipulate the DOM yourself. This saves you a lot of time, and gives you a nice clean separation between your web app's logic and its user interface.

Knockout's main features

Knockout revolves around the following main features:

  • The view: The HTML document that displays the user interface.
  • The view model: A JavaScript object that represents all the data and operations in the view.
  • Bindings: data-bind attributes that you add to various HTML elements in the view. The bindings link those elements to the data in the view model.
  • Observables: Values you add to the view model that have a two-way binding to an element, or elements, in the view. When an observable value in the view model changes, so does the view, and vice-versa.
  • Computed observables: Observables that compute their values based on the values of other observables. Knockout cleverly tracks dependencies, so that whenever an observable's value changes, any computed observables that use that observable are run automatically to compute their new values.
  • Templates: Snippets of HTML markup that Knockout inserts into the view as required. For example, a template might only be inserted if a certain condition is true, or you can create a loop to repeatedly insert a template containing an li element into a list.

Let's take a look at some of these key features, starting with views, view models, and bindings.

Creating views, view models and bindings

Building blocks

Here's a very simple example Knockout page that creates a view, a view model, and a binding between the two:

<!doctype html> 
<html> 
  <head> 
  <title>A Basic Knockout View, View Model, and Binding</title> 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript">

function ViewModel() {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Create a variable that the view can bind to */
  self.productName = "WonderWidget";
}

/* When the DOM is ready, activate Knockout */

$( function() {
  ko.applyBindings( new ViewModel() );
} );

  </script>
</head>
<body> 
  <h1>A Basic Knockout View, View Model, and Binding<br></h1>
  <p>Your selected product is: <span data-bind="text: productName"></span>.</p>
</body>
</html>

Press the button below to see this page in action:

As you can see, the page displays the following:


Your selected product is: WonderWidget.

Let's take a look at the various elements in the page:

  1. The JavaScript includes
    Inside the page's head element, we've added a couple of JavaScript includes: jQuery on Google's CDN, which we'll use in the examples in this tutorial, and the Knockout library itself, which is stored in a single JavaScript file, knockout-2.1.0.js.

    Download the Knockout library via the Knockout installation page.

  2. The view
    The view is the user interface. In this case, it's simply an h1 element with the page title, and a p element containing the message "Your selected product is: ", along with a span element to hold the product name.
  3. The view model
    The JavaScript code at the top of the document creates a view model class, ViewModel. The class contains a single property, productName, holding the value "WonderWidget". After creating the ViewModel class, the code uses the jQuery $() method to wait until the DOM is ready before calling the Knockout method ko.applyBindings(), passing in a new ViewModel object. This activates Knockout and associates the view model with the current view — that is, the web page.
  4. The binding
    On line 28, our view's span element has the attribute data-bind="text: productName". This tells Knockout to link the text inside the span with the value of the productName property in the view model object. When our code calls ko.applyBindings(), Knockout goes through the markup, finds the data-bind attribute, and inserts productName's value inside the span.

At the top of the ViewModel class (line 12), we assign the JavaScript pseudo-variable this (which, at that point, represents the current object) to a local variable called self. Then we use self whenever we want to refer to the current object, rather than this.

Why do we do this? Well, in JavaScript the value of this can change depending on the context in which the object's functions are called. This happens frequently when working with Knockout. By assigning this to self at the start of the class, we ensure that we'll always have a reference to the current object. For more details on this convention, see "Managing 'this'" on Knockout's Computed Observables page, and this good discussion on Stack Overflow.

Adding observables

Binoculars

We've now created a basic Knockout example, with a view, a view model, and a binding to link the two together. However, this page isn't interactive at all — it doesn't do anything after it's displayed.

To take our example to the next level, it would be great if we could let the user interact with the page after it's loaded. For example, the user could click a button to change the value of the productName property. Then the span's text in the view should update itself accordingly. Let's try it:

<!doctype html> 
<html> 
  <head> 
  <title>Changing a Property in the View Model - First Attempt</title> 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript">

function ViewModel() {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Create a variable that the view can bind to */
  self.productName = "WonderWidget";

  /* Change the product when the user clicks the button */
  $('#changeProduct').click( function() {
    self.productName = "SuperWidget";
  } );
}

/* When the DOM is ready, activate Knockout */

$( function() {
  ko.applyBindings( new ViewModel() );
} );

  </script>
</head> 
<body> 
  <h1>Changing a Property in the View Model - First Attempt<br></h1>
  <p>Your selected product is: <span data-bind="text: productName"></span>.</p>
  <button id="changeProduct">Change Product</button>
</body>
</html>

As you can see, we've added a Change Product button to the page, then created a click handler using jQuery that changes the value of the view model's productName property to "SuperWidget".

Try out the example by pressing the button below:

Press the Change Product button in the page. Did anything happen? No!

Although the productName property does change to "SuperWidget" when the button is clicked, the span's text doesn't update automatically to reflect the new value. This is because Knockout has no way of knowing that the property's value has changed.

Doing it right

To fix this problem, we need to replace our productName property with — you guessed it — an observable. An observable is a value that Knockout can track, so that whenever the value changes in the view model — even after the page has loaded — Knockout automatically updates any bound HTML elements in the view.

You create an observable in your view model like this:


observableName = ko.observable( initialValue );

This creates a new observable, observableName, with an optional starting value, initialValue. (If you miss out initialValue then the observable's value is undefined.)

You then read an observable's value like this:


currentValue = observableName();

...and change the observable's value like this:


observableName( newValue );

Within your view, you access an observable through a data-bind attribute, just like you reference a regular property:


data-bind="text: observableName"

OK, now we can rewrite our simple example above, replacing the productName property with a productName observable:

<!doctype html> 
<html> 
  <head> 
  <title>Creating an Observable</title> 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript">

function ViewModel() {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Create an observable that the view can bind to */
  self.productName = ko.observable( "WonderWidget" );
  
  /* Change the product when the user clicks the button */
  $('#changeProduct').click( function() {
    self.productName( "SuperWidget" );
  } );
}

/* When the DOM is ready, activate Knockout */

$( function() {
  ko.applyBindings( new ViewModel() );
} );

  </script>
</head> 
<body> 

  <h1>Creating an Observable<br></h1>
  <p>Your selected product is: <span data-bind="text: productName"></span>.</p>
  <button id="changeProduct">Change Product</button>
</body>
</html>

I've highlighted the important changes in the code above. As you can see, we create a new observable called self.productName inside the view model, and give it the initial value "WonderWidget". Then, in the click handler, we change the observable's value to "SuperWidget".

Try it out by pressing the button below:

Notice that, when you click the Change Product button, the text in the page changes to:


Your selected product is: SuperWidget.

As you can see, this change happens automatically. We don't need to explicitly write any code to update the DOM. This is one of Knockout's main strengths.

Two-way observables

Observables get even better. If you create a binding between an observable and an HTML element that the user can update — for example, a text field — then whenever the user changes the value in the element, Knockout updates the observable value in the view model automatically. This means you can easily create a two-way link between the data in your view model and the widgets in the view, all without having to write tedious JavaScript code to access the DOM. Nice!

Let's try this out. We'll remove the Change Product button from our example, and replace it with a text field bound directly to the productName observable:

<!doctype html> 
<html> 
  <head> 
  <title>Creating a Two-Way Observable</title> 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript">

function ViewModel() {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Create an observable that the view can bind to */
  self.productName = ko.observable( "WonderWidget" );
}

/* When the DOM is ready, activate Knockout */

$( function() {
  ko.applyBindings( new ViewModel() );
} );

  </script>
</head> 
<body> 

  <h1>Creating a Two-Way Observable<br></h1>
  
  <p>Your selected product is: <span data-bind="text: productName"></span>.</p>

  <label for="newProductName">New Product Name: </label>
  <input type="text" name="newProductName" data-bind="value: productName" />

</body>
</html>

As you can see on line 33, we've added an <input type="text"> field to the page. The field includes the attribute data-bind="value: productName". This binds the text field's value to the productName observable.

Go ahead and try it out:

Try typing a new value, such as "My Great Widget", into the text field and pressing Return. Notice that the text in the page automatically updates to:


Your selected product is: My Great Widget.

When you change the value in the text field, this automatically updates the productName observable. This, in turn, automatically updates the paragraph of text in the page.

More on bindings

In addition to the text and value bindings, there are a range of other useful bindings that you can use to link HTML elements in the view with observables in the view model. We'll look at some of these throughout the tutorial. Here's a quick summary of the built-in Knockout bindings:

visible
Hides the element if the observable's value is false.
text
Makes the element display the observable's text value.
html
Makes the element render the HTML string stored in the observable.
css
Adds or removes a CSS class on the element, based on the result of an expression.
style
Adds or removes an inline CSS style on the element, based on the result of an expression.
attr
Sets any attribute of the element to the observable's value.
foreach
Loops through an array, duplicating a chunk of markup for each array item. Handy for lists and tables.
if
Displays a chunk of markup only if an expression is true.
ifnot
Displays a chunk of markup only if an expression is false.
with
Lets you explicitly set a binding context for a chunk of markup.
click
Assigns a specified JavaScript function to be the click event handler for the element.
event
Assigns a specified JavaScript function to be a specified event handler for the element.
submit
Assigns a specified JavaScript function to be the submit event handler for a form element.
enable
Enables a form field if the observable's value is true, and disables it if the value is false.
disable
Disables a form field if the observable's value is true, and enables it if the value is false.
value
Creates a two-way binding between an observable and a form field's value. This includes text fields, textareas, and select menus.
hasfocus
If the observable is set to true, this binding focuses the element. If it's set to false, the binding unfocuses the element. Similarly, if the user focuses or unfocuses the element, the observable becomes true or false.
checked
Links a checkbox or radio button's checked status with an observable. If the observable is set to true or false then the element is checked or unchecked automatically. Similarly, if the user checks or unchecks the element then the observable is set to true or false respectively.
options
Populates a select menu's options with the values in an array (or an observable array).
selectedOptions
Creates a two-way binding between an observable array and the selected options in a multiple select menu.
uniqueName
Adds a unique name attribute to the element, if it doesn't already have one. Handy for plugins and browsers that require an element to have a name attribute.
template
Lets you explicitly set an element's content to the results of rendering a named template (that is, a chunk of markup, usually containing data-bind attributes).

You can even write your own custom bindings.

Find out more about bindings in the Knockout documentation.

Creating observable arrays

Safe

An observable array allows your view model to keep track of a whole group of items at once. This is handy if your user interface contains user-editable lists of items, such as select menus, tables, or groups of checkboxes. You can easily bind an observable array to such elements, and let Knockout take care of updating the contents of the array as the user adds and removes items in the list.

You create an observable array like this:


observableArrayName = ko.observableArray( initialValue );

You can pass an optional initial array in order to pre-populate the observable array. For example:


selectedItems = ko.observableArray( [1,2,3] );

If you don't supply an initial array then the observable array starts out empty.

Once you've created your observable array, you can access the underlying JavaScript array by calling the observable array as a function, like this:


observableArrayName()

For example, you can display the first element inside a selectedItems observable array like this:


alert( 'The first selected item is ' + selectedItems()[0] );

Although you can manipulate the underlying array using JavaScript's array functions, such as push(), splice() and indexOf(), Knockout provides its own equivalent functions that you can use directly on the observable array. Not only are these functions more convenient, but they also work across all browsers and link up with Knockout's dependency tracking, so that when you modify an array the corresponding elements in the view get updated automatically.

For example, you can use push() add a new item to the end of an observable array, like this:


observableArrayName.push( newItem );

You'll find a full list of Knockout's observable array functions on this page.

Let's try an example that shows observable arrays in practice:

<!doctype html> 
<html> 
  <head> 
  <title>Using Observable Arrays</title> 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript">

function ViewModel() {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Create an array to store the available products */
  self.availableProducts = ko.observableArray( [ "SuperWidget", "MegaWidget", "WonderWidget" ] );

  /* Create an observable array to store the selected products */
  self.selectedProducts = ko.observableArray();

  /* Track the available product that the user wants to add */
  self.productToAdd = ko.observable();

  /* Track the available product that the user wants to remove */
  self.productToRemove = ko.observable();

  /* Add a product to the "selected products" list */
  self.addProduct = function() {
    self.selectedProducts.push( self.productToAdd() );
    self.productToRemove( self.productToAdd() );
    self.availableProducts.remove( self.productToAdd() );
    self.productToAdd( self.availableProducts()[0] );
  }

  /* Remove a product from the "selected products" list */
  self.removeProduct = function() {
    self.availableProducts.push( self.productToRemove() );
    self.productToAdd( self.productToRemove() );
    self.selectedProducts.remove( self.productToRemove() );
    self.productToRemove( self.selectedProducts()[0] );
  }
}

/* When the DOM is ready, activate Knockout */
$( function() {
  ko.applyBindings( new ViewModel() );
} );

  </script>
</head> 
<body> 

  <h1>Using Observable Arrays<br></h1>

  <label for="availableProducts">Available Products:</label>
  <select name="availableProducts" id="availableProducts" data-bind="options: availableProducts, value: productToAdd" size="3" style="width: 120px"></select>
  <button data-bind="click: addProduct, enable: productToAdd">Add Product</button>

  <br/><br/>

  <label for="selectedProducts">Selected Products:</label>
  <select name="selectedProducts" id="selectedProducts" data-bind="options: selectedProducts, value: productToRemove" size="3" style="width: 120px"></select>
  <button data-bind="click: removeProduct, enable: productToRemove">Remove Product</button>

</body>
</html>

This example contains two select menus: a list of available products, and a list of selected products. You can select a product in the first list and press the Add Product button to move the product to the second list. Similarly, you can select a product in the second list and press Remove Product to move it back to the first list.

Try it out by pressing the following button:

The page contains the following parts:

  • The availableProducts select menu
    This holds the list of products that the user can select from. It has an options binding that binds the options in the menu to the availableProducts observable array (which we'll look at in a moment). This means that whenever an item is added to or removed from the observable array, the options in the select menu update automatically.

    The menu also has a value binding that links the selected item with the productToAdd observable. So whenever the user selects an item, productToAdd automatically updates to contain the selected product name. Conversely, if the productToAdd observable is changed in the view model, the selected item in the select menu changes automatically to match.
  • The Add Product button
    After the Available Products menu is an Add Product button that adds the selected product to the Selected Products menu. This button has two bindings: click, which calls an addProduct() method when the button is pressed, and enable, which enables the button when the productToAdd observable contains a non-empty value (and disables it if productToAdd is empty).
  • The selectedProducts select menu and Remove Product button
    These two widgets are the counterparts of the availableProducts select menu and Add Product button. selectedProducts holds the list of products that the user has added, and contains bindings to the selectedProducts observable array and the productToRemove observable. The Remove Product button contains a click binding to the removeProduct() method to remove a product from the list, and an enable binding that enables the button only if the productToRemove observable is non-empty.
  • The availableProducts and selectedProducts observable arrays
    In the JavaScript at the top of the page, we create the two observable arrays: availableProducts to hold the list of products to select (initially populated with the values "SuperWidget", "MegaWidget" and "WonderWidget"), and selectedProducts to hold the list of products that the user has selected (initially empty). Since these are bound to the select menus in the view, the select menus will update automatically whenever items are added to or removed from these arrays.
  • The productToAdd and productToRemove observables
    Next we create two observables, productToAdd and productToRemove. These are bound to the availableProducts and selectedProducts select menus respectively, using their value bindings. This means that, when the user selects an item in one of the menus, the corresponding observable value is automatically updated. Similarly, if we update the observable, the corresponding value in the menu is automatically selected.
  • The addProduct() method
    Our view model contains an addProduct() method that runs when the user presses the Add Product button. The method's job is to move the selected product from the availableProducts menu to the selectedProducts menu. The method does this in four steps:

    1. It adds the selected product — stored in the productToAdd observable — to the selectedProducts observable array. (The select menu updates automatically.)
    2. It selects the newly-added item in the selectedProducts menu by setting the productToRemove observable's value to the name of the added product.
    3. It removes the added product from the availableProducts observable (and therefore the select menu).
    4. It selects the first item in the availableProducts menu by setting the productToAdd observable to the first product in the availableProducts observable array.
  • The removeProduct() method
    This method runs when the user presses the Remove Product button. It is the exact opposite of the addProduct() method: it moves the selected product in the selectedProducts menu back to the availableProducts menu.

With this example, you can see how powerful observable arrays can be. By linking observable arrays to select menus in the view, you can let Knockout take care of the tedious work of updating the menus when the view model changes, and vice-versa.

You can use observable arrays with other "list" UI elements too. Later in the tutorial, you'll see how to bind an observable array to a group of checkboxes.

Creating computed observables

Safe

As well as holding simple values and arrays of values, observables can also be functions that calculate and return a value. These are called computed observables. You create a computed observable like this:


observableName = ko.computed( function() {

  // Do stuff here to compute the value
  return ( value );

} );

Computed observables have a very important feature. Say a computed observable uses the values of some other observables to carry out its calculation. If any of these observables' values change, Knockout automatically re-runs your computed observable so that its value is updated too.

For example:

  1. You create a computed observable, A, that uses the value of another observable, B, and another computed observable, C, to compute its value.
  2. The computed observable C uses two other observables, D and E, to compute its value.
  3. If B's value changes, Knockout automatically re-runs A to compute its new value.
  4. If either D's or E's value changes (or both values change), Knockout automatically re-runs C to compute its new value. Since C's value has now also changed, Knockout also re-runs A to compute its new value.

In this way, you can chain as many observables and computed observables together as you like, and Knockout handles all the dependencies automatically.

Let's try an example that uses computed observables:

<!doctype html> 
<html> 
  <head> 
  <title>Creating Computed Observables</title> 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript">

function ViewModel() {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Create some regular observables to bind to the view */
  self.firstName = ko.observable( "" );
  self.lastName = ko.observable( "" );
  self.location = ko.observable( "" );

  /* Create a computed observable to construct the full name */
  self.fullName = ko.computed( function() {
    return ( self.firstName() + " " + self.lastName() );
  } );

  /* Create another computed observable to construct the greeting */
  self.greeting = ko.computed( function() {
    if ( self.fullName().length > 1 && self.location().length > 0 ) {
      return ( "Hello, " + self.fullName() + " in " + self.location() + "!" );
    } else {
      return ( "Please enter your first and last name, and your location." );
    }
  } );

}

/* When the DOM is ready, activate Knockout */

$( function() {
  ko.applyBindings( new ViewModel() );
} );

  </script>
</head> 
<body> 

  <h1>Creating Computed Observables<br></h1>

  <p data-bind="text: greeting"></p>

  <label for="firstName">Your First Name: </label>
  <input type="text" name="firstName" data-bind="value: firstName" />
  <br/>

  <label for="lastName">Your Last Name: </label>
  <input type="text" name="lastName" data-bind="value: lastName" />
  <br/>

  <label for="location">Your Location: </label>
  <input type="text" name="location" data-bind="value: location" />
  <br/>

</body>
</html>

Try it out by pressing the button below:

To start with, you'll see a prompt telling you to enter your first and last name, and your location. Fill in the three text fields in the page, and press Return. Notice that the prompt changes to a greeting that includes the data you entered into the fields.

Let's take a look at our code and see how it works:

  • The view
    The view contains a paragraph element whose text value is bound to an observable called greeting. It also contains the three text fields — firstName, lastName and location — whose values are bound to the firstName, lastName and location observables respectively.
  • The standard observables
    On lines 15-17, our view model defines three regular Knockout observables, firstName, lastName and location, and gives them each an initial value of "" (an empty string).
  • The fullName computed observable
    On lines 20-22, we've added a computed observable called fullName to our view model. This function simply concatenates the values of the firstName and lastName observables (with a space in between), and returns the resulting string. Whenever the value of either firstName or lastName changes, fullName is automatically run to compute the new value.
  • The greeting computed observable
    On lines 25-31, we've added a second computed observable, greeting. This function constructs the greeting message to display to the user, based on the value of the fullName computed observable, as well as the value of the location observable.

    If either the fullName or location values are empty, greeting returns the "Please enter your..." prompt instead.

    Again, if the value of either fullName or location changes, Knockout recognizes the dependency and automatically re-runs greeting's function to compute the new value. What's more, since the paragraph in the view is bound to the greeting computed observable, the moment greeting's value changes, the text in the paragraph automatically updates to the new value.

As you can see, computed observables make it really easy to create complex user interfaces. When the user changes one widget in the page, a bunch of other widgets in the page can update automatically, without you having to write the code to explicitly manage the dependencies. This is powerful stuff!

By default, computed observables are "read-only" — a widget in the view can't update the value of a computed observable, since you can't change the value of a function. However, two-way computed observables are possible, and rather useful. See the section "Writeable computed observables" in Knockout's Computed Observables page.

Under the hood, Knockout implements all the bindings in your view as computed observables. So whenever an observable value changes in the view model, the relevant values and widgets in the view update automatically.

A complete example: Widget Shop

Safe

Let's bring together many of the concepts you've learned in this tutorial and build "Widget Shop", a complete product selection page using Knockout. Our page will have the following features:

  • The page pulls some JSON product data from the server via Ajax, and displays four product thumbnails, along with the product names.
  • When the user clicks a thumbnail, a "product info" box appears, containing the product name, price, full-size product image, description, options, a Quantity field, and a Buy Now button.
  • The user can select different product options and change the quantity. The price inside the Buy Now button updates automatically.
  • When the user presses the Buy Now button, the selected product data is sent to the server via Ajax. (In our example, we'll simulate this by simply displaying the JSON data in an alert box.)

You can try out the example now by pressing the following button:

Try clicking a product to select it, then adjusting the options and quantity. When you're done, press the Buy Now button to see the JSON data that the app would send to the server.

The product data file

Let's start with the JSON product data. Normally this would be pulled dynamically from a database using a server-side script (such as PHP), but to keep things simple we'll create a static text file instead. Create a widget-shop folder somewhere in your website, and save this file as products.txt inside the widget-shop folder:
{
  "products": [

    {
      "id": 1,
      "name": "SuperWidget",
      "price": 19.99,
      "description": "The SuperWidget is a great all-rounder. Simple, fast, and it gets the job done.",
      "thumbImage": "SuperWidget-small.jpg",
      "mainImage": "SuperWidget.jpg",
      "options": [
        { "optionId": 1, "name": "Large Screen", "price": 7  },
        { "optionId": 2, "name": "64GB Memory", "price": 9 },
        { "optionId": 3, "name": "Fast Charger", "price": 3 }
      ]
    },

    {
      "id": 2,
      "name": "WonderWidget",
      "price": 24.99,
      "description": "If you want a string of admirers following you around, go for the WonderWidget. Its eye-catching bright orange colour and striking lines will catch anybody's attention!",
      "thumbImage": "WonderWidget-small.jpg",
      "mainImage": "WonderWidget.jpg",
      "options": [
        { "optionId": 4, "name": "Day-Glow Paintwork", "price": 3 },
        { "optionId": 5, "name": "Turbo Booster", "price": 11 },
        { "optionId": 6, "name": "WonderWidget Baseball Cap", "price": 2 }
      ]
    },

    {
      "id": 3,
      "name": "MegaWidget",
      "price": 29.99,
      "description": "The maximum bang for your buck. The MegaWidget comes with 128GB memory as standard, and enough power to scare a yak 18 miles away.",
      "thumbImage": "MegaWidget-small.jpg",
      "mainImage": "MegaWidget.jpg",
      "options": [
        { "optionId": 7, "name": "Sonic Enhancer", "price": 18 },
        { "optionId": 8, "name": "Heavy-Duty Battery Pack", "price": 13 },
        { "optionId": 9, "name": "MegaWidget Bumper Sticker", "price": 1 }
      ]
    },

    {
      "id": 4,
      "name": "HyperWidget",
      "price": 49.99,
      "description": "The most luxurious widget money can buy. Carved from a single block of amazium, you can drop this widget off a cliff and still use it the next day. It has 72 USB ports and a 30-day battery life, and comes with a 5-year warranty.",
      "thumbImage": "HyperWidget-small.jpg",
      "mainImage": "HyperWidget.jpg",
      "options": [
        { "optionId": 10, "name": "Leather Case", "price": 5 },
        { "optionId": 11, "name": "Table Stand", "price": 3 },
        { "optionId": 12, "name": "Solar Charger", "price": 14 }
      ]
    }

  ]

}

If you're familiar with JSON then this file should be pretty straightforward. It represents four products: SuperWidget, WonderWidget, MegaWidget and HyperWidget. Each product has various attributes, including a unique ID, a name, a price, a description, thumbnail and full-size image filenames, and an options array containing a list of product options, each with a unique ID, a name, and a price.

If you're not familiar with JSON, check out my JSON Basics tutorial for a gentle introduction.

The view

Now let's create our view — that is, the page that the user interacts with. Save the following file as choose-product.html inside your widget-shop folder:

<!doctype html> 
<html> 
  <head> 
  <title>Complete Knockout Demo: Product Selection</title> 
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1" />
  <link rel="stylesheet" href="style.css" />
  <script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
  <script type="text/javascript" src="knockout-2.1.0.js"></script>
  <script type="text/javascript" src="choose-product.js"></script>

  <script type="text/javascript">
    $( getProducts );
  </script>

</head> 
<body> 

  <h1>Complete Knockout Demo: Product Selection<br></h1>

  <div id="content">
   
    <h2>Choose a Product:</h2>

    <ul id="products" data-bind="foreach: products">
      <li data-bind="click: $parent.chooseProduct, css: { selected: $parent.productSelected(id) }">
        <img data-bind="attr: { src: 'images/' + thumbImage, alt: name }"/><br/>
        <span data-bind="text: name"></span>
      </li>
    </ul>

    <div id="chosenProduct" data-bind="visible: chosenProduct">

      <h2><span data-bind="text: chosenProduct().name"></span> <span id="productPrice" data-bind="text: '$'+chosenProduct().price"></span></h2>
      <img data-bind="attr: { src: 'images/' + chosenProduct().mainImage, alt: chosenProduct().name }"/>

      <div id="productInfo">

        <p data-bind="text: chosenProduct().description"></p>

        <h3>Customize Your Product:</h3>

        <ul data-bind="foreach: chosenProduct().options">
          <li data-bind="css: { selected: $parent.optionSelected(optionId) }">
            <label onclick="function() {}"><input type="checkbox" data-bind="value: optionId, checked: $parent.chosenOptions"/><span data-bind="text: name"></span> <span>($</span><span data-bind="text: price"></span><span>)</span></label>
          </li>
        </ul>

        Quantity: <input type="number" name="qty" id="qty" data-bind="value: qty" min="1" max="9"/>
        <button id="buy" data-bind="click: buyProduct">Buy Now ($<span data-bind="text: totalPrice"></span>)</button>

      </div>

    </div>

  </div>

</body>
</html>

Let's look at the important parts of this page:

  • The includes
    As well as our usual jQuery and Knockout includes, we also include a style sheet, style.css, and a choose-product.js JavaScript file that will contain our view model and other functions. (We'll create these files in a moment.)
  • The call to getProducts()
    When the page DOM is ready, we use jQuery's $() method on line 13 to call getProducts(), a function within choose-product.js that grabs the product data and starts Knockout running.
  • The product thumbnails
    On lines 25-30, we create a ul element to hold the list of products. We add a foreach binding to the element that links a products array in the view model with the list. This makes Knockout loop over the products in the array, rendering the supplied li element template for each product object in the array.

    We add two bindings to the li element itself: a click binding that runs the view model's chooseProduct() method when the user clicks the product, and a css binding that calls the view model's productSelected() method, passing in the current product object's id property. If this method returns true, the binding adds the selected CSS class to the li to highlight the selected product.

    The special Knockout property $parent refers to the parent of the products array — that is, the view model object. Find out more about $parent, and other special properties, in the Knockout documentation.

    Within the li, we add an img element with an attr binding. The binding adds two attributes to the element: src, to link the element to the product's thumbnail image file (stored inside an images folder), and alt, to set the image's alternate text to the product name. It gets these two pieces of data from the current product object's thumbImage and name properties. We also add a span element that displays the product's name, again pulled from the object's name property.

    You'll find the sample product images in the images folder inside the code zip file.

  • The chosenProduct div
    The div with the id of chosenProduct, on lines 32-54, displays the selected product details. We add a visible binding to it so that it's only shown when the chosenProduct observable is not empty — that is, the user has chosen a product.
  • The product name and base price
    Within the chosenProduct div, we first add an h2 element on line 34 containing two spans. The first span displays the selected product's name by retrieving the name property of the product object stored in the chosenProduct observable in the view model. The second span displays the product's price by retrieving the product object's price property.
  • The main product image
    Next, on line 35, we add an img element to the chosenProduct div to display the main product image. As with the thumbnail, we add a src attribute pointing to the image in the images folder, and an alt attribute containing the product name. We pull this data from the currently-chosen product's mainImage and name properties respectively.
  • The productInfo div
    This div, on lines 37-52, contains the product description, options, Quantity field, and Buy Now button.
  • The product description
    On line 39, we add the product's long description inside the productInfo div as a p element. The element has a text binding to the currently-chosen product's description property.
  • The product options
    On lines 41-47, we add a "Customize Your Product" heading, along with a ul element containing the product's options. Much like the product thumbnails above, we use a foreach binding to the chosen product's options array to display one li per option. We add a css binding to each li that calls the view model's optionSelected() method to determine if the current option is selected; if it is then we add a selected CSS class to the li to highlight it.

    On line 45, within each li, we add a checkbox with two bindings:

    • value, which we bind to the current option object's optionId property
    • checked, which we bind to the view model's chosenOptions observable array. Knockout then associates the checkboxes with this array, so that whenever the user checks or unchecks a checkbox, the checkbox's value is added to or removed from the chosenOptions array automatically. Nice!
    The li also contains the option's text, pulled from the current option object's name property, and the option's price, pulled from the price property. We wrap the whole lot in a label element so the user can click anywhere in the li to toggle the checkbox.

    The onclick handler that we've added to the label element works around a known bug in Mobile Safari that prevents a label from being tapped to select a form field.

  • The Quantity field
    On line 49, we add a Quantity field allowing the user to choose how many of the product they'd like to buy. We bind its value to a qty observable in the view model.
  • The Buy Now button
    Finally, on line 50 our view includes a Buy Now button. We set the view model's buyProduct() method as a click handler for this button. We also insert the total price into the button's text using the totalPrice computed observable. This observable, which you'll create in a moment, calculates the total price on the fly based on the selected product, any chosen options, and the value of the Quantity field.

The view model and other JavaScript

The other important part of our widget shop is the JavaScript to create the view model object and fetch the product data from the server. Save the following file as choose-product.js inside your widget-shop folder:


/* Get the product data from the server, then preload the product images
 * and apply the Knockout bindings */

function getProducts() {

  var images = new Array();
  
  $.getJSON( "products.txt", null, function( ajaxData ) {

    for ( var i in ajaxData.products ) {
      images[i] = new Image();
      images[i].src = "images/" + ajaxData.products[i].mainImage;
    }

    ko.applyBindings( new ChooseProductViewModel( ajaxData.products ) );
  } ); 
}

/* The Knockout View Model object */

function ChooseProductViewModel( productData ) {

  /* Store 'this' in 'self' so we can use it throughout the object */
  var self = this;

  /* Store the retrieved list of product objects in the view model
   * so that our view can access it
   */
  self.products = productData;

  /* Create Knockout observables for various parts of our view */
  self.chosenProduct = ko.observable( false );   /* The currently-chosen product object */
  self.chosenOptions = ko.observableArray();     /* The currently-chosen options array */
  self.qty = ko.observable( 1 );                 /* The currently-entered quantity value */

  /* Compute the total order price */

  self.totalPrice = ko.computed( function() {

    /* Grab the currently-chosen product object */
    var product = self.chosenProduct();

    /* If no product has been chosen yet, do nothing */
    if ( !product ) return false;

    /* Store the base product price */
    var price = product.price;

    /* Add the price of each chosen option to the overall price */

    var chosenOptions =  self.chosenOptions();

    for ( i=0; i<product.options.length; i++ ) {
      for ( j=0; j<chosenOptions.length; j++ ) {
        if ( product.options[i].optionId == chosenOptions[j] ) {
          price += product.options[i].price;
          break;
        }
      }
    }

    /* Return the total price multiplied by the chosen quantity */
    return ( price * self.qty() ).toFixed( 2 );
  } );


  /* Change the chosen product and scroll down to the "chosen product" box */

  self.chooseProduct = function( product ) {
    self.chosenProduct( product );
    $('html,body').animate( { scrollTop: $("#chosenProduct").offset().top }, 'slow' );
  }


  /* Determine if the supplied option has been selected by the user */

  self.optionSelected = function( optionId ) {

    var chosenOptions =  self.chosenOptions();
    selected = false;

    for ( j=0; j<chosenOptions.length; j++ ) {
      if ( optionId == chosenOptions[j] ) {
        selected = true;
        break;
      }
    }

    return selected;
  }


  /* Determine if the supplied product has been selected by the user */

  self.productSelected = function( productId ) {
    return ( productId == self.chosenProduct().id );
  }


  /* Send the product data to the server */

  self.buyProduct = function() {

    /* Extract just the selected options for the chosen product */
    var product = self.chosenProduct();
    var chosenOptions = self.chosenOptions();
    var chosenOptionsForProduct = [];

    for ( i=0; i<product.options.length; i++ ) {
      for ( j=0; j<chosenOptions.length; j++ ) {
        if ( product.options[i].optionId == chosenOptions[j] ) {
          chosenOptionsForProduct.push( product.options[i].optionId );
          break;
        }
      }
    }

    /* Compose the data object */
    var data = {
      "chosenProductId": self.chosenProduct().id,
      "chosenOptions": chosenOptionsForProduct,
      "qty": self.qty()
    }

    /* Send the data to the server */
    alert( "Data to send to server:\n\n" + JSON.stringify( data ) );
  }
   
}

Let's work through each part of this code:

  • The getProducts() function
    This function is called from the choose-product.html page when the DOM is ready. First it uses jQuery's getJSON() method to pull the JSON product data from the products.txt file on the server and turn it into a JavaScript object.

    Once the data is loaded and the data object has been created, the function preloads all the main product images by storing them in an array of Image objects; this ensures that the user doesn't have to wait for the main product images to load. The function then creates a new ChooseProductViewModel object and passes the data object's products property to the constructor so that the view model object can store the product data. Then it calls Knockout's ko.applyBindings() method, passing in the new view model object, to start the ball rolling.
  • The view model
    The rest of the JavaScript file defines the ChooseProductViewModel class that we use to create our view model object. On line 21, we start the class definition, accepting the array of product objects in a productData parameter. Let's take a look at each chunk of code in our view model...
  • Store 'this' in 'self'
    As usual, on line 24, we store the this special variable in the self local variable so we always have a reference to the current object.
  • Store the retrieved list of product objects in the view model
    On line 29, we store the product data passed to the view model in a local self.products property, so that we can easily access it from the view.
  • Create Knockout observables for various parts of our view
    On lines 32-34, we create three observables to link to our view:

    • chosenProduct will store the currently-selected product object. We set this to false initially. This is used throughout the view to access information about the chosen product.
    • chosenOptions is an observable array holding the IDs of any product options that the user has selected. This array is linked to the option checkboxes in the view.
    • qty maps to the Quantity field in the view. It's set to 1 initially.
  • The totalPrice computed observable
    On lines 38-64 we define a computed observable, totalPrice, that calculates the total price for the Buy Now button on the fly. This function retrieves the currently-chosen product and stores its base price in a local price variable. Then it loops through the available options in the currently-chosen product, as well as all the selected options in the chosenOptions observable array. Each time it finds a product option that has been selected, it adds its price to the price variable. Finally, the function multiples the total price by the value of the qty observable, rounds the resulting value to 2 decimal places, and returns the final price.

    In theory you could replace the inner for loop in this function with Knockout's indexOf() observable array method. However, I found this method to be somewhat buggy in Firefox 12, so I manually looped through the array instead.

  • The chooseProduct() method
    The chooseProduct() method, defined on lines 69-72, runs when the user clicks a product thumbnail. It accepts the chosen product object as a parameter, product, then sets the chosenProduct observable to this object. Since all the widgets inside the chosenProduct div in the view are bound to the chosenProduct observable, changing this observable's value automatically updates all of those widgets.

    On line 71, we also use jQuery to slowly scroll down to the chosenProduct div in the view. This creates a nice smooth transition, especially on mobile phones.
  • The optionSelected() method
    On lines 77-87, we define an optionSelected() method, which determines if a given product option has been selected by the user. The method accepts an optionId argument. It then loops through all the selected options in the chosenOptions observable array. If it finds the optionId value in the array then it returns true; otherwise it returns false.

    This method is bound to the product option li elements using the css binding. If the method returns true then the CSS class selected is applied to the li, highlighting it. If it returns false then the CSS class is removed.
  • The productSelected() method
    Lines 95-97 define a productSelected() method. Much like the optionSelected() method, productSelected() returns true if the product with the supplied ID has been selected by the user, and false if it hasn't. It simply compares the supplied ID against the ID of the product object stored in the chosenProduct observable. This method is bound to the product thumbnail li elements using a css binding, so that the selected class is added to the li if the product is selected, highlighting the li element.
  • The buyProduct() method
    Finally, on lines 102-127 we define a buyProduct() method, which runs when the user presses the Buy Now button in the view. The method simply creates a data object containing three properties:

    • chosenProductId, pulled from the id property of the product object stored in the chosenProduct observable.
    • chosenOptions, which we calculate as the intersection between the chosenOptions observable array and the options associated with the chosen product (so that we only include selected options for the currently-selected product).
    • qty, pulled from the qty observable.
    Once it's constructed the data object, the method turns it into a JSON string by calling the JavaScript JSON.stringify() method. In a real-world app, the method would then send the JSON string to the server via Ajax. For this demo, we simply display the string in an alert box.

The stylesheet

To make our Widget Shop page look nice — and adapt to different screen sizes, from desktop down to mobile phone — we need to add a stylesheet. Save the following file as style.css in your widget-shop folder:

/* Add some margin to the page and set a default font and colour */

body {
  margin: 30px;
  font-family: "Georgia", serif;
  color: #333;
}

/* Give headings and other elements their own font */

h1, h2, h3, h4, #products li, #productInfo li {
  font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}

h1 {
  font-size: 120%;
}

/* Main content area */

#content {
  margin: 40px auto;
  -moz-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  padding: 20px 0;
  background: #C7CCE8;
  overflow: hidden;
  max-width: 1340px;
}

#content h2, #content h3 {
  margin-left: 20px;
}

/* Product list */

#products {
  list-style: none;
  overflow: hidden;
  margin: 0;
  padding: 0;
}

#products li {
  display: block;
  float: left;
  width: 270px;
  margin: 0 0 20px 20px;
  text-align: center;
  background: rgba(148,155,187,.5);
  font-size: 1.2em;
  font-weight: bold;
  padding: 30px 20px 20px 20px;
  cursor: pointer;
}

#products li.selected {
  background: #fff;
}

#products li img {
  width: 150px;
  height: 150px;
  border: 1px solid #999;
  margin-bottom: 10px;
}

/* Chosen Product box */

#chosenProduct {
  margin-left: 20px;
}

#chosenProduct h2 {
  margin: 0;
  padding: 10px 0 10px 20px;
  width: 620px;
  background: rgba(255,255,255,.6);
}

#chosenProduct img {
  width: 300px;
  height: 300px;
  border: 10px solid #fff;
  float: left;
  display: block;
}

#productPrice {
  font-size: 80%;
  font-weight: normal;
  margin-left: 10px;
}

#productInfo {
  float: left;
  width: 300px;
  height: 300px;
  padding: 10px;
  background: #fff;
}

#productInfo p {
  margin-top: 0;
  font-size: 90%;
  height: 76px;
}

#productInfo h3 {
  margin: 20px 0 10px 0;
  padding: 0;
  line-height: 1.2em;
}

/* Product options */

#productInfo ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

#productInfo ul li {
  background: rgba(148,155,187,.5);
  color: #555;
  margin: 4px 0;
  overflow: hidden;
  line-height: 1.3em;
}

#productInfo ul li.selected {
  background: #A0AFF6;
  color: #000;
}

#productInfo ul li input[type="checkbox"] {
  margin: 0 6px;
  vertical-align: middle;
}

#productInfo ul li label {
  display: block;
  width: 100%;
  padding: 8px 4px;
  float: left;
  cursor: pointer;
}

#productInfo ul li label span {
  vertical-align: middle;
}

/* Quantity field and Buy Now button */

#qty {
  border: 1px solid #999;
  width: 40px;
  padding: 6px;
  font-size: 15px;
  margin-top: 10px;
  line-height: 16px;
}

#buy {
  background: #fc7;
  border: 1px solid #999;
  padding: 6px;
  width: 170px;
  float: right;
  font-weight: bold;
  font-size: 15px;
  margin-top: 10px;
  line-height: 18px;
  cursor: pointer;
}

/* Media queries for responsive layout */

@media screen and (max-width: 1400px) {

  #content {
    width: 1010px;
  }

}

@media screen and (max-width: 1070px) {

  #content {
    width: 680px;
  }

}

@media screen and (max-width: 700px) {

  #content {
    max-width: 360px;
    margin-left: auto;
    margin-right: auto;
  }

  #products li {
    width: 280px;
  }

  #chosenProduct h2 {
    width: 300px;
  }

  #chosenProduct img, #productInfo {
    float: none;
  }

}

@media screen and (max-width: 320px) {

  body {
    margin: 0;
    padding: 0;
  }

  h1 {
    margin: 10px;
    text-align: center;
  }

  body p {
    text-align: center;
  }

  #content {
    max-width: 320px;
    margin: 0;
    padding: 0;
  }

  #products li {
    width: 240px;
    margin-left: 20px;
  }

  #chosenProduct {
    margin-left: 0;
  }


  #content h2, #content h3 {
    margin-left: 0;
    padding-left: 0;
    width: 320px;
    text-align: center;
  }

  #productInfo h3 {
    margin-left: -10px;
  }

}

Without going into too much detail, this stylesheet does the following main things:

  • It sets up basic styles for the page, such as margins, fonts and colours.
  • It styles the main #content div.
  • It styles the list of product thumbnails, putting each thumbnail in a box that can be highlighted with a selected class.
  • It formats the "chosen product" box, styling the box itself, as well as the product name and price, main image, description, product options, Quantity field and Buy Now button.
  • It adds media queries to adjust the page layout at various different browser sizes, from widescreen desktop displays to mobile phone displays.

Try it out!

If you haven't already done so, try out the Widget Shop example by pressing the button below:

Click a product thumbnail, and notice how the chosen product's details appear at the bottom of the page. Try selecting product options by clicking the checkboxes under Customize Your Product, and entering different values in the Quantity field. Try selecting different products in the list. At any time, you can click the Buy Now button to see the JSON data that would be sent to the server.

Notice how various page elements — including the highlighting on the thumbnails and product options, the chosen product's details, and the price inside the Buy Now button — all update automatically as you interact with the widgets in the page. As you've seen, we've managed to do all this without writing any JavaScript code to update the DOM. Such is the magic of Knockout!

Summary

In this tutorial you've explored Knockout, a fantastic JavaScript library that lets you create rich, interactive user interfaces without needing to write tons of tedious JavaScript code. You've learned:

  • How Knockout works, and the concept of the Model-View-Viewmodel design pattern.
  • How to create a Knockout view (web page) and a view model object, and link the two together with bindings
  • How to use observables to create values in the view model that automatically update in the view, and vice-versa
  • How to use observable arrays to track a group of values at once
  • How to create computed observables to calculate values on the fly and create chains of dependent values

At the end of the tutorial, you brought all these concepts together and built a complete product selection page using Knockout, with a nice, interactive user interface that automatically adapts as the user selects different options.

I hope you enjoyed reading this tutorial, and found it useful. Have fun with Knockout!

[Photo credits: Gramody, Minnesota Historical Society, ToadLickr, Creative Tools]

[Widget Shop photo credits: cocoate.com, Johan Larsson, wlodi, mwichary]

Follow Elated

Related articles

Responses to this article

3 responses (oldest first):

25-Jun-12 19:50
Thanks for such a comprehensive article on Knockout...
I'm currently checking out the various options with all these wonderful Javascript frameworks available. But one thing that has me stumped is how to integrate them with database's (receiving the data, updating the data etc...)
Almost all of them say something like you have:
"Normally this would be pulled dynamically from a database using a server-side script (such as PHP), but to keep things simple we'll create a static text file instead"
But it seems difficult to find a real world example which demonstrates how this is done.
Hope you can provide some feedback on this...
Best Regards, Dave
26-Jun-12 02:34
@daveporter: Thanks for your comment. That's a good question. Essentially you'd have, for example, a MySQL database on the server, and a PHP script that runs a SELECT query on the DB to pull out the product data, then format it as a JSON string using http://php.net/manual/en/function.json-encode.php and send that back to the browser.

To go the other way, you'd use http://php.net/manual/en/function.json-decode.php to decode the JSON string that currently appears in the alert box when you press Buy Now, then do something useful with it (such as create a record in an 'orders' table etc).

Hard to summarise in a couple of paragraphs! Maybe I'll write an "end-to-end" tutorial explaining the process at some point...
26-Jun-12 02:51
Cheers Matt - I'll look forward to that...
Regards, Dave

Post a response

Want to add a comment, or ask a question about this article? Post a response.

To post responses you need to be a member. Not a member yet? Signing up is free, easy and only takes a minute. Sign up now.

Top of Page