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.
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:
- You create your HTML view; that is, your user interface, or web page, containing text messages, text fields, checkboxes,
select
menus, and so on. - 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.
- Then, any changes you make to the data in the view model object automatically update the widgets in the view.
- 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

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:
- The JavaScript includes
Inside the page’shead
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.
- The view
The view is the user interface. In this case, it’s simply anh1
element with the page title, and ap
element containing the message “Your selected product is: “, along with aspan
element to hold the product name. - 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 theViewModel
class, the code uses the jQuery$()
method to wait until the DOM is ready before calling the Knockout methodko.applyBindings()
, passing in a newViewModel
object. This activates Knockout and associates the view model with the current view — that is, the web page. - The binding
On line 28, our view’sspan
element has the attributedata-bind="text: productName"
. This tells Knockout to link the text inside thespan
with the value of theproductName
property in the view model object. When our code callsko.applyBindings()
, Knockout goes through the markup, finds thedata-bind
attribute, and insertsproductName
‘s value inside thespan
.
Adding observables

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 aform
element.
enable
- Enables a form field if the observable’s value is
true
, and disables it if the value isfalse
.
disable
- Disables a form field if the observable’s value is
true
, and enables it if the value isfalse
.
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 tofalse
, the binding unfocuses the element. Similarly, if the user focuses or unfocuses the element, the observable becomestrue
orfalse
.
checked
- Links a checkbox or radio button’s
checked
status with an observable. If the observable is set totrue
orfalse
then the element is checked or unchecked automatically. Similarly, if the user checks or unchecks the element then the observable is set totrue
orfalse
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 aname
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.
Creating observable arrays

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 );
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 anoptions
binding that binds the options in the menu to theavailableProducts
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 theselect
menu update automatically.The menu also has a
value
binding that links the selected item with theproductToAdd
observable. So whenever the user selects an item,productToAdd
automatically updates to contain the selected product name. Conversely, if theproductToAdd
observable is changed in the view model, the selected item in theselect
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 anaddProduct()
method when the button is pressed, andenable
, which enables the button when theproductToAdd
observable contains a non-empty value (and disables it ifproductToAdd
is empty). - The
selectedProducts
select
menu and Remove Product button
These two widgets are the counterparts of theavailableProducts
select
menu and Add Product button.selectedProducts
holds the list of products that the user has added, and contains bindings to theselectedProducts
observable array and theproductToRemove
observable. The Remove Product button contains aclick
binding to theremoveProduct()
method to remove a product from the list, and anenable
binding that enables the button only if theproductToRemove
observable is non-empty. - The
availableProducts
andselectedProducts
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"
), andselectedProducts
to hold the list of products that the user has selected (initially empty). Since these are bound to theselect
menus in the view, theselect
menus will update automatically whenever items are added to or removed from these arrays. - The
productToAdd
andproductToRemove
observables
Next we create two observables,productToAdd
andproductToRemove
. These are bound to theavailableProducts
andselectedProducts
select
menus respectively, using theirvalue
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 anaddProduct()
method that runs when the user presses the Add Product button. The method’s job is to move the selected product from theavailableProducts
menu to theselectedProducts
menu. The method does this in four steps:- It adds the selected product — stored in the
productToAdd
observable — to theselectedProducts
observable array. (Theselect
menu updates automatically.) - It selects the newly-added item in the
selectedProducts
menu by setting theproductToRemove
observable’s value to the name of the added product. - It removes the added product from the
availableProducts
observable (and therefore theselect
menu). - It selects the first item in the
availableProducts
menu by setting theproductToAdd
observable to the first product in theavailableProducts
observable array.
- It adds the selected product — stored in the
- The
removeProduct()
method
This method runs when the user presses the Remove Product button. It is the exact opposite of theaddProduct()
method: it moves the selected product in theselectedProducts
menu back to theavailableProducts
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.
Creating computed observables

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:
- You create a computed observable, A, that uses the value of another observable, B, and another computed observable, C, to compute its value.
- The computed observable C uses two other observables, D and E, to compute its value.
- If B‘s value changes, Knockout automatically re-runs A to compute its new value.
- 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 calledgreeting
. It also contains the three text fields —firstName
,lastName
andlocation
— whose values are bound to thefirstName
,lastName
andlocation
observables respectively. - The standard observables
On lines 15-17, our view model defines three regular Knockout observables,firstName
,lastName
andlocation
, 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 calledfullName
to our view model. This function simply concatenates the values of thefirstName
andlastName
observables (with a space in between), and returns the resulting string. Whenever the value of eitherfirstName
orlastName
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 thefullName
computed observable, as well as the value of thelocation
observable.Again, if the value of either
fullName
orlocation
changes, Knockout recognizes the dependency and automatically re-runsgreeting
‘s function to compute the new value. What’s more, since the paragraph in the view is bound to thegreeting
computed observable, the momentgreeting
‘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!
A complete example: Widget Shop

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.
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>($lt;/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 ($lt;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 achoose-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 callgetProducts()
, a function withinchoose-product.js
that grabs the product data and starts Knockout running. - The product thumbnails
On lines 25-30, we create aul
element to hold the list of products. We add aforeach
binding to the element that links aproducts
array in the view model with the list. This makes Knockout loop over the products in the array, rendering the suppliedli
element template for each product object in the array.We add two bindings to the
li
element itself: aclick
binding that runs the view model’schooseProduct()
method when the user clicks the product, and acss
binding that calls the view model’sproductSelected()
method, passing in the current product object’sid
property. If this method returnstrue
, the binding adds theselected
CSS class to theli
to highlight the selected product.Within the
li
, we add animg
element with anattr
binding. The binding adds two attributes to the element:src
, to link the element to the product’s thumbnail image file (stored inside animages
folder), andalt
, to set the image’s alternate text to the product name. It gets these two pieces of data from the current product object’sthumbImage
andname
properties. We also add aspan
element that displays the product’s name, again pulled from the object’sname
property. - The
chosenProduct
div
Thediv
with theid
ofchosenProduct
, on lines 32-54, displays the selected product details. We add avisible
binding to it so that it’s only shown when thechosenProduct
observable is not empty — that is, the user has chosen a product. - The product name and base price
Within thechosenProduct
div
, we first add anh2
element on line 34 containing twospan
s. The firstspan
displays the selected product’s name by retrieving thename
property of the product object stored in thechosenProduct
observable in the view model. The secondspan
displays the product’s price by retrieving the product object’sprice
property. - The main product image
Next, on line 35, we add animg
element to thechosenProduct
div
to display the main product image. As with the thumbnail, we add asrc
attribute pointing to the image in theimages
folder, and analt
attribute containing the product name. We pull this data from the currently-chosen product’smainImage
andname
properties respectively. - The
productInfo
div
Thisdiv
, 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 theproductInfo
div
as ap
element. The element has atext
binding to the currently-chosen product’sdescription
property. - The product options
On lines 41-47, we add a “Customize Your Product” heading, along with aul
element containing the product’s options. Much like the product thumbnails above, we use aforeach
binding to the chosen product’soptions
array to display oneli
per option. We add acss
binding to eachli
that calls the view model’soptionSelected()
method to determine if the current option is selected; if it is then we add aselected
CSS class to theli
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’soptionId
propertychecked
, which we bind to the view model’schosenOptions
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 thechosenOptions
array automatically. Nice!
The
li
also contains the option’s text, pulled from the current option object’sname
property, and the option’s price, pulled from theprice
property. We wrap the whole lot in alabel
element so the user can click anywhere in theli
to toggle the checkbox. -
- 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 aqty
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’sbuyProduct()
method as aclick
handler for this button. We also insert the total price into the button’s text using thetotalPrice
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:nn" + JSON.stringify( data ) ); } }
Let’s work through each part of this code:
- The
getProducts()
function
This function is called from thechoose-product.html
page when the DOM is ready. First it uses jQuery’sgetJSON()
method to pull the JSON product data from theproducts.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 newChooseProductViewModel
object and passes the data object’sproducts
property to the constructor so that the view model object can store the product data. Then it calls Knockout’sko.applyBindings()
method, passing in the new view model object, to start the ball rolling. - The view model
The rest of the JavaScript file defines theChooseProductViewModel
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 aproductData
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 thethis
special variable in theself
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 localself.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 tofalse
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 localprice
variable. Then it loops through the available options in the currently-chosen product, as well as all the selected options in thechosenOptions
observable array. Each time it finds a product option that has been selected, it adds its price to theprice
variable. Finally, the function multiples the total price by the value of theqty
observable, rounds the resulting value to 2 decimal places, and returns the final price. - The
chooseProduct()
method
ThechooseProduct()
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 thechosenProduct
observable to this object. Since all the widgets inside thechosenProduct
div
in the view are bound to thechosenProduct
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 anoptionSelected()
method, which determines if a given product option has been selected by the user. The method accepts anoptionId
argument. It then loops through all the selected options in thechosenOptions
observable array. If it finds theoptionId
value in the array then it returnstrue
; otherwise it returnsfalse
.This method is bound to the product option
li
elements using thecss
binding. If the method returnstrue
then the CSS classselected
is applied to theli
, highlighting it. If it returnsfalse
then the CSS class is removed. - The
productSelected()
method
Lines 95-97 define aproductSelected()
method. Much like theoptionSelected()
method,productSelected()
returnstrue
if the product with the supplied ID has been selected by the user, andfalse
if it hasn’t. It simply compares the supplied ID against the ID of the product object stored in thechosenProduct
observable. This method is bound to the product thumbnailli
elements using acss
binding, so that theselected
class is added to theli
if the product is selected, highlighting theli
element. - The
buyProduct()
method
Finally, on lines 102-127 we define abuyProduct()
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 theid
property of the product object stored in thechosenProduct
observable.chosenOptions
, which we calculate as the intersection between thechosenOptions
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 theqty
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]
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
@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…
Cheers Matt – I’ll look forward to that…
Regards, Dave
Hello.
Great tutorial.
I am trying to add some levels the the shop json . how can i loop through sub levels. ie
this only gets the top level
thanks
all demos seems to be down, because jquery lib is referenced via http instead of https
Demo works for me: https://www.elated.com/res/File/articles/development/javascript/knockout-tutorial/widget-shop/choose-product.html