Observables to refresh the UI automatically
The last example shows us how Knockout binds data and the user interface, but it doesn't show the magic of the automatic UI refresh. To perform this task, Knockout uses observables.
Observables are the main concept of Knockout. These are special JavaScript objects that can notify subscribers about changes, and can automatically detect dependencies. For compatibility, ko.observable
objects are actually functions.
To read an observable's current value, just call the observable with no parameters. In this example, product.price()
will return the price of the product, and product.name()
will return the name of the product.
var product = Product(1,"T-Shirt", 10.00, 20); product.price();//returns 10.00 product.name();//returns "T-Shirt"
To write a new value to the observable, call the observable and pass the new value as a parameter. For example, calling product.name('Jeans')
will change the name
value to 'Jeans'
.
var product = Product(1,"T-Shirt", 10.00, 20); product.name();//returns "T-Shirt" product.name("Jeans");//sets name to "Jeans" product.name();//returns "Jeans"
The complete documentation about observables is on the official Knockout website http://knockoutjs.com/documentation/observables.html.
To show how observables work, we are going to add some input data into our template.
Add these HTML tags over div
that contain product information.
<div> <strong>ID:</strong> <input class="form-control" type="text" data-bind="value:product.id"/><br/> <strong>Name:</strong> <input class="form-control" type="text" data-bind="value:product.name"><br/> <strong>Price:</strong> <input class="form-control" type="text" data-bind="value:product.price"/><br/> <strong>Stock:</strong> <input class="form-control" type="text" data-bind="value:product.stock"><br/> </div>
We have linked inputs to the view-model using the value
property. Run the code and try to change the values in the inputs. What happened? Nothing. This is because variables are not observables. Update your product.js
file, adding the ko.observable
method to each variable:
"use strict"; function Product(id, name, price, stock) { "use strict"; var _id = ko.observable(id), _name = ko.observable(name), _price = ko.observable(price), _stock = ko.observable(stock) ; return { id:_id, name:_name, price:_price, stock:_stock }; }
Notice that when we update the data inside the inputs, our product values are updated automatically. When you change the name
value to Jeans
, the text binding will automatically update the text content of the associated DOM element. That's how changes to the view-model automatically propagate to the view.
Managing collections with observables
If you want to detect and respond to changes in one object, you'd use observables. If you want to detect and respond to changes in a collection of things, use an observableArray
. This is useful in many scenarios where you're displaying or editing multiple values and need repeated sections of the UI to appear and disappear as items are added and removed.
To display a collection of products in our application, we are going to follow some simple steps:
- Open the
index.html
file and remove the code inside the<body>
tag and then add a table where we will list our catalog:<h1>Catalog</h1> <table class="table"> <thead> <tr> <th>Name</th> <th>Price</th> <th>Stock</th> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> </tr> </tbody> </table>
- Define an array of products inside the view-model:
"use strict"; var vm = (function () { var catalog = [ Product(1, "T-Shirt", 10.00, 20), Product(2, "Trousers", 20.00, 10), Product(3, "Shirt", 15.00, 20), Product(4, "Shorts", 5.00, 10) ]; return { catalog: catalog }; })(); ko.applyBindings(vm);
- Knockout has a binding to repeat a piece of code for each element in a collection. Update the
tbody
element in the table:<tbody data-bind="foreach:catalog"> <tr> <td data-bind="text:name"></td> <td data-bind="text:price"></td> <td data-bind="text:stock"></td> </tr> </tbody>
We use the foreach
property to point out that all that is inside this tag should be repeated for each item in the collection. Inside this tag we are in the context of each element, so you can just bind properties directly. Observe the result in your browser.
We want to know how many items we have in our catalog, so add this line of code above the table:
<strong>Items:</strong> <span data-bind="text:catalog.length"></span>
Inserting elements in collections
To insert elements in the products array, an event should occur. In this case, the user will click on a button and this action will fire an action that will insert a new product in the collection.
In future chapters, you will learn more about events. Now we will just need to know that there is a binding property named click
. It receives a function as a parameter, and this function is fired when the user clicks on the element.
To insert an element, we need a form to insert the values of the new product. Write this HMTL code just below the <h1>
tag:
<form class="form-horizontal" role="form" data-bind="with:newProduct">
<div class="form-group">
<div class="col-sm-12">
<input type="text" class="form-control" placeholder="Name" data-bind="textInput:name">
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<input type="password" class="form-control" placeholder="Price" data-bind="textInput:price">
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<input type="password" class="form-control" placeholder="Stock" data-bind="textInput:stock">
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-default" data-bind="{click:$parent.addProduct}">
<i class="glyphicon glyphicon-plus-sign">
</i> Add Product
</button>
</div>
</div>
</form>
In this template, we find some new bindings:
- The
with
binding: This creates a new binding context so that descendant elements are bound in the context of a specified object, in this casenewProduct
. - The
textInput
binding: ThetextInput
binding links a textbox (<input>
) or text area (<textarea>
) with a view-model property, providing two-way updates between theviewmodel
property and the element's value. Unlike thevalue
binding property,textInput
provides instant updates from the DOM for all types of user input, including autocomplete, drag-and-drop, and clipboard events. It is available from the 3.2 version of Knockout. - The
click
binding: Theclick
binding adds an event handler so that your chosen JavaScript function is invoked when the associated DOM element is clicked. When calling your handler, Knockout will supply the current model value as the first parameter. This is particularly useful if you're rendering UI for each item in a collection, and you need to know which item's UI was clicked. - The
$parent
object: This is a binding context property. We use it to refer to data from outside theforeach
loop.
For more information about binding context properties, read the Knockout documentation at http://knockoutjs.com/documentation/binding-context.html.
Now it is time to add the newProduct
object to our view-model. First we should define a new product with empty data:
var newProduct = Product("","","","");
We have defined a literal object that will contain the information we want to put inside our new product. Also, we have defined a method to clear or reset the object once the insertion is done. Now we define our addProduct
method:
var addProduct = function (context) { var id = new Date().valueOf();//random id from time var newProduct = Product( id, context.name(), context.price(), context.stock() ); catalog.push(newProduct); newProduct.clear(); };
This method creates a new product with the data received from the click event.
The click event always sends the context as the first argument. Note also that you can use array methods such as push
in an observable array. Check out the Knockout documentation (http://knockoutjs.com/documentation/observableArrays.html) to see all the methods available in arrays.
We should implement the private method that will clean data from the new product once it is added to the collection:
var clearNewProduct = function () { newProduct.name(""); newProduct.price(""); newProduct.stock(""); };
Update the view-model:
return { catalog: catalog, newProduct: newProduct, addProduct: addProduct };
If you run the code, you will notice that when you try to add a new product nothing happens. This is because, despite the fact that our products have observable properties, our array is not an observable one. For this reason, Knockout is not listening to the changes. We should convert the array to an observableArray
observable.
var catalog = ko.observableArray([ Product(1, "T-Shirt", 10.00, 20), Product(2, "Trousers", 20.00, 10), Product(3, "Shirt", 15.00, 20), Product(4, "Shorts", 5.00, 10) ]);
Now Knockout is listening to what is going on with this array, but not what is happening inside each element. Knockout just tells us about inserting or deleting elements in the array, but not about editing elements. If you want to know what is happening in an element, the object should have observable properties.
An observableArray
observable just tracks which objects it holds, and notifies listeners when objects are added or removed.
Behind the scenes, the observableArray
is actually an observable whose value is an array. So you can get the underlying JavaScript array by invoking the observableArray
observable as a function with no parameters, just like any other observable. Then you can read information from that underlying array.
<strong>Items:</strong> <span data-bind="text:catalog().length"></span>
Computed observables
It is not weird to think that some values we show in our interface depend on other values that Knockout is already observing. For example, if we would like to search products in our catalog by name, it is evident that the products in the catalog that we show in the list are related to the term we have entered in the search box. In these cases Knockout offers us computed observables.
You can learn in detail about computed observables in the Knockout documentation at http://knockoutjs.com/documentation/computedObservables.html.
To develop the search function, define a textbox where we can write a term to search. We are going to bind it to the searchTerm
property. To update the value as we write, we should use the textInput
binding. If we use the value binding, the value will be updated when the element loses the focus. Put this code over the products table:
<div class="input-group"> <span class="input-group-addon"> <i class="glyphicon glyphicon-search"></i> Search</span> <input type="text" class="form-control" data-bind="textInput: searchTerm"> </div>
To create a filtered catalog, we are going to check all our items and test if the searchTerm
is in the item's name
property.
var searchTerm = ko.observable(''); var filteredCatalog = ko.computed(function () { //if catalog is empty return empty array if (!catalog()) { return []; } var filter = searchTerm().toLowerCase(); //if filter is empty return all the catalog if (!filter) { return catalog(); } //filter data var filtered = ko.utils.arrayFilter(catalog(), function (item) { var fields = ["name"]; //we can filter several properties var i = fields.length; while (i--) { var prop = fields[i]; var strProp = ko.unwrap(item[prop]).toLocaleLowerCase(); if (strProp.indexOf(filter) !== -1){ return true; }; } Return false; }); return filtered; });
The ko.utils
object is not documented in Knockout. It is an object used by the library internally. It has public access and has some functions that can help us with observables. There are a lot of unofficial examples about it on the Internet.
One of its helpful functions is ko.utils.arrayFilter
. If you look at line 13, we have used this method to obtain a filtered array.
This function gets an array as the first parameter. Notice that we invoke the catalog
array observable to get the elements. We don't pass the observable itself, but the contents of the observable.
The second parameter is the function that decides whether the item will be in the filtered array or not. It will return true
if the item has the conditions to be in the filtered array. Otherwise it returns false
.
On line 14 of this snippet, we can find an array called fields
. This parameter will contain the fields that should comply with the criteria. In this case, we just check that the filter value is in the name
value. If we are pretty sure that we are just going to check the name
field, we can simplify the filter function:
var filtered = ko.utils.arrayFilter(catalog(), function (item) { var strProp = ko.unwrap(item["name"]).toLocaleLowerCase(); return (strProp.indexOf(filter) > -1); });
The ko.unwrap
function returns the value that contains the observable. We use ko.unwrap
when we are not sure if the variable contains an observable or not, for example:
var notObservable = 'hello'; console.log(notObservable()) //this will throw an error. console.log(ko.unwrap(notObservable)) //this will display 'hello');
Expose the filtered catalog into the public API. Notice that now we need to use the filtered catalog instead of the original catalog of products. Because we are applying the revealing module pattern, we can keep the original API interface and just update the value of the catalog with the filtered catalog. We don't need to alert the view that we are going to use a different catalog or other element, as long as we always maintain the same public interface:
return { searchTerm: searchTerm, catalog: filteredCatalog, newProduct: newProduct, addProduct: addProduct };
Now, try to type some characters in the search box and see in your browser how the catalog updates the data automatically.
Wonderful! We have completed our first three user stories:
- The user should be able to view the catalog
- The user should be able to search the catalog
- The user should be able to add items to the catalog
Let's see the final result: