- KnockoutJS Essentials
- Jorge Ferrando
- 2471字
- 2021-07-23 20:16:16
Creating templates
Template elements are commonly declared at the bottom of the body, just above the <script>
tags that have references to our external libraries. We are going to define some templates and then we will talk about each one of them:
<!-- templates --> <script type="text/html" id="header"></script> <script type="text/html" id="catalog"></script> <script type="text/html" id="add-to-catalog-modal"></script> <script type="text/html" id="cart-widget"></script> <script type="text/html" id="cart-item"></script> <script type="text/html" id="cart"></script> <script type="text/html" id="order"></script> <script type="text/html" id="finish-order-modal"></script>
Each template name is descriptive enough by itself, so it's easy to know what we are going to set inside them.
Let's see a diagram showing where we dispose each template on the screen:

Notice that the cart-item
template will be repeated for each item in the cart collection. Modal templates will appear only when a modal dialog is displayed. Finally, the order
template is hidden until we click to confirm the order.
In the header
template, we will have the title and the menu of the page. The catalog
template will contain the table with products we wrote in Chapter 1, Refreshing the UI Automatically with KnockoutJS. The add-to-catalog-modal
template will contain the modal that shows the form to add a product to our catalog. The cart-widget
template will show a summary of our cart. The cart-item
template will contain the template of each item in the cart. The cart
template will have the layout of the cart. The order
template will show the final list of products we want to buy and a button to confirm our order.
The header template
Let's begin with the HTML markup that should contain the header
template:
<script type="text/html" id="header"> <h1> Catalog </h1> <button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#addToCatalogModal"> Add New Product </button> <button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, css:{ disabled: cart().length < 1}"> Show Cart Details </button> <hr/> </script>
We define a <h1>
tag, and two <button>
tags.
The first button tag is attached to the modal that has the ID #addToCatalogModal
. Since we are using Bootstrap as the CSS framework, we can attach modals by ID using the data-target
attribute, and activate the modal using the data-toggle
attribute.
The second button will show the full cart view and it will be available only if the cart has items. To achieve this, there are a number of different ways.
The first one is to use the CSS-disabled class that comes with Twitter Bootstrap. This is the way we have used in the example. CSS binding allows us to activate or deactivate a class in the element depending on the result of the expression that is attached to the class.
The other method is to use the enable
binding. This binding enables an element if the expression evaluates to true
. We can use the opposite binding, which is named disable
. There is a complete documentation on the Knockout website http://knockoutjs.com/documentation/enable-binding.html:
<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, enable: cart().length > 0"> Show Cart Details </button> <button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, disable: cart().length < 1"> Show Cart Details </button>
The first method uses CSS classes to enable and disable the button. The second method uses the HTML attribute, disabled
.
We can use a third option, which is to use a computed observable. We can create a computed observable variable in our view-model that returns true
or false
depending on the length of the cart:
//in the viewmodel. Remember to expose it
var cartHasProducts = ko.computed(function(){
return (cart().length > 0);
});
//HTML
<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, enable: cartHasProducts">
Show Cart Details
</button>
To show the cart, we will use the click
binding in the same way we used it in the previous chapter.
Now we should go to our viewmodel.js
file and add all the information we need to make this template work:
var cart = ko.observableArray([]); var showCartDetails = function () { if (cart().length > 0) { $("#cartContainer").removeClass("hidden"); } };
And you should expose these two objects in the view-model:
return { //first chapter searchTerm: searchTerm, catalog: filteredCatalog, newProduct: newProduct, totalItems:totalItems, addProduct: addProduct, //second chapter cart: cart, showCartDetails: showCartDetails, };
The catalog template
The next step is to define the catalog
template just below the header
template:
<script type="text/html" id="catalog">
<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>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Stock</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach:catalog">
<tr data-bind="style:color:stock() < 5?'red':'black'">
<td data-bind="text:name"></td>
<td data-bind="text:price"></td>
<td data-bind="text:stock"></td>
<td>
<button class="btn btn-primary" data-bind="click:$parent.addToCart">
<i class="glyphicon glyphicon-plus-sign"></i> Add
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">
<strong>Items:</strong><span data-bind="text:catalog().length"></span>
</td>
<td colspan="1">
<span data-bind="template:{name:'cart-widget'}"></span>
</td>
</tr>
</tfoot>
</table>
</script>
This is the same table we built in the previous chapter. We have just added a few new things:
<tr data-bind="style:{color: stock() < 5?'red':'black'}">...</tr>
Now, each line uses the style
binding to alert the user, while they are shopping, that the stock is reaching the maximum limit. The style
binding works the same way that CSS binding does with classes. It allows us to add style attributes depending on the value of the expression. In this case, the color of the text in the line must be black if the stock is higher than five, and red if it is four or less. We can use other CSS attributes, so feel free to try other behaviors. For example, set the line of the catalog to green if the element is inside the cart. We should remember that if an attribute has dashes, you should wrap it in single quotes. For example, background-color
will throw an error, so you should write 'background-color'
.
When we work with bindings that are activated depending on the values of the view-model, it is good practice to use computed observables. Therefore, we can create a computed value in our product model that returns the value of the color that should be displayed:
//In the Product.js var _lineColor = ko.computed(function(){ return (_stock() < 5)? 'red' : 'black'; }); return { lineColor:_lineColor }; //In the template <tr data-bind="style:lineColor"> ... </tr>
It would be even better if we create a class in our style.css
file that is called stock-alert
and we use the CSS binding:
//In the style file .stock-alert { color: #f00; } //In the Product.js var _hasStock = ko.computed(function(){ return (_stock() < 5); }); return { hasStock: _hasStock }; //In the template <tr data-bind="css: hasStock"> ... </tr>
Now, look inside the <tfoot>
tag.
<td colspan="1"> <span data-bind="template:{name:'cart-widget'}"></span> </td>
As you can see, we can have nested templates. In this case, we have the cart-widget
template inside our catalog
template. This give us the possibility of having very complex templates, splitting them into very small pieces, and combining them, to keep our code clean and maintainable.
Finally, look at the last cell of each row:
<td>
<button class="btn btn-primary" data-bind="click:$parent.addToCart">
<i class="glyphicon glyphicon-plus-sign"></i> Add
</button>
</td>
Look at how we call the addToCart
method using the magic variable $parent
. Knockout gives us some magic words to navigate through the different contexts we have in our app. In this case, we are in the catalog
context and we want to call a method that lies one level up. We can use the magical variable called $parent
.
There are other variables we can use when we are inside a Knockout context. There is complete documentation on the Knockout website http://knockoutjs.com/documentation/binding-context.html.
In this project, we are not going to use all of them. But we are going quickly explain these binding context variables, just to understand them better.
If we don't know how many levels deep we are, we can navigate to the top of the view-model using the magic word $root
.
When we have many parents, we can get the magic array $parents
and access each parent using indexes, for example, $parents[0]
, $parents[1]
. Imagine that you have a list of categories where each category contains a list of products. These products are a list of IDs and the category has a method to get the name of their products. We can use the $parents
array to obtain the reference to the category:
<ul data-bind="foreach: {data: categories}"> <li data-bind="text: $data.name"></li> <ul data-bind="foreach: {data: $data.products, as: 'prod'}> <li data-bind="text: $parents[0].getProductName(prod.ID)"></li> </ul> </ul>
Look how helpful the as
attribute is inside the foreach
binding. It makes code more readable. But if you are inside a foreach
loop, you can also access each item using the $data
magic variable, and you can access the position index that each element has in the collection using the $index
magic variable. For example, if we have a list of products, we can do this:
<ul data-bind="foreach: cart"> <li><span data-bind="text:$index"> </span> - <span data-bind="text:$data.name"></span> </ul>
This should display:
0 – Product 1
1 – Product 2
2 – Product 3
...

KnockoutJS magic variables to navigate through contexts
Now that we know more about what binding variables are, let's go back to our code. We are now going to write the addToCart
method.
We are going to define the cart items in our js/models
folder. Create a file called CartProduct.js
and insert the following code in it:
//js/models/CartProduct.js var CartProduct = function (product, units) { "use strict"; var _product = product, _units = ko.observable(units); var subtotal = ko.computed(function(){ return _product.price() * _units(); }); var addUnit = function () { var u = _units(); var _stock = _product.stock(); if (_stock === 0) { return; } _units(u+1); _product.stock(--_stock); }; var removeUnit = function () { var u = _units(); var _stock = _product.stock(); if (u === 0) { return; } _units(u-1); _product.stock(++_stock); }; return { product: _product, units: _units, subtotal: subtotal, addUnit : addUnit, removeUnit: removeUnit, }; };
Each cart product is composed of the product itself and the units of the product we want to buy. We will also have a computed field that contains the subtotal of the line. We should give the object the responsibility for managing its units and the stock of the product. For this reason, we have added the addUnit
and removeUnit
methods. These methods add one unit or remove one unit of the product if they are called.
We should reference this JavaScript file into our index.html
file with the other <script>
tags.
In the view-model, we should create a cart array and expose it in the return statement, as we have done earlier:
var cart = ko.observableArray([]);
It's time to write the addToCart
method:
var addToCart = function(data) { var item = null; var tmpCart = cart(); var n = tmpCart.length; while(n--) { if (tmpCart[n].product.id() === data.id()) { item = tmpCart[n]; } } if (item) { item.addUnit(); } else { item = new CartProduct(data,0); item.addUnit(); tmpCart.push(item); } cart(tmpCart); };
This method searches the product in the cart. If it exists, it updates its units, and if not, it creates a new one. Since the cart is an observable array, we need to get it, manipulate it, and overwrite it, because we need to access the product object to know if the product is in the cart. Remember that observable arrays do not observe the objects they contain, just the array properties.
The add-to-cart-modal template
This is a very simple template. We just wrap the code we made in Chapter 1, Refreshing the UI Automatically with KnockoutJS, to add a product to a Bootstrap modal:
<script type="text/html" id="add-to-catalog-modal"> <div class="modal fade" id="addToCatalogModal"> <div class="modal-dialog"> <div class="modal-content"> <form class="form-horizontal" role="form" data-bind="with:newProduct"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> <span aria-hidden="true">×</span> <span class="sr-only">Close</span> </button><h3>Add New Product to the Catalog</h3> </div> <div class="modal-body"> <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="text" class="form-control" placeholder="Price" data-bind="textInput:price"> </div> </div> <div class="form-group"> <div class="col-sm-12"> <input type="text" class="form-control" placeholder="Stock" data-bind="textInput:stock"> </div> </div> </div> <div class="modal-footer"> <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> </div> </form> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal --> </script>
The cart-widget template
This template gives the user information quickly about how many items are in the cart and how much all of them cost:
<script type="text/html" id="cart-widget"> Total Items: <span data-bind="text:totalItems"></span> Price: <span data-bind="text:grandTotal"></span> </script>
We should define totalItems
and grandTotal
in our view-model:
var totalItems = ko.computed(function(){ var tmpCart = cart(); var total = 0; tmpCart.forEach(function(item){ total += parseInt(item.units(),10); }); return total; }); var grandTotal = ko.computed(function(){ var tmpCart = cart(); var total = 0; tmpCart.forEach(function(item){ total += (item.units() * item.product.price()); }); return total; });
Now you should expose them in the return statement, as we always do. Don't worry about the format now, you will learn how to format currency or any kind of data in the future. Now you must focus on learning how to manage information and how to show it to the user.
The cart-item template
The cart-item
template displays each line in the cart:
<script type="text/html" id="cart-item"> <div class="list-group-item" style="overflow: hidden"> <button type="button" class="close pull-right" data-bind="click:$root.removeFromCart"><span>×</span></button> <h4 class="" data-bind="text:product.name"></h4> <div class="input-group cart-unit"> <input type="text" class="form-control" data-bind="textInput:units" readonly/> <span class="input-group-addon"> <div class="btn-group-vertical"> <button class="btn btn-default btn-xs" data-bind="click:addUnit"> <i class="glyphicon glyphicon-chevron-up"></i> </button> <button class="btn btn-default btn-xs" data-bind="click:removeUnit"> <i class="glyphicon glyphicon-chevron-down"></i> </button> </div> </span> </div> </div> </script>
We set an x button in the top-right of each line to easily remove a line from the cart. As you can see, we have used the $root
magic variable to navigate to the top context because we are going to use this template inside a foreach
loop, and it means this template will be in the loop context. If we consider this template as an isolated element, we can't be sure how deep we are in the context navigation. To be sure, we go to the right context to call the removeFormCart
method. It's better to use $root
instead of $parent
in this case.
The code for removeFromCart
should lie in the view-model context and should look like this:
var removeFromCart = function (data) { var units = data.units(); var stock = data.product.stock(); data.product.stock(units+stock); cart.remove(data); };
Notice that in the addToCart
method, we get the array that is inside the observable. We did that because we need to navigate inside the elements of the array. In this case, Knockout observable arrays have a method called remove
that allows us to remove the object that we pass as a parameter. If the object is in the array, it will be removed.
Remember that the data context is always passed as the first parameter in the function we use in the click events.
The cart template
The cart
template should display the layout of the cart:
<script type="text/html" id="cart"> <button type="button" class="close pull-right" data-bind="click:hideCartDetails"> <span>×</span> </button> <h1>Cart</h1> <div data-bind="template: {name: 'cart-item', foreach:cart}" class="list-group"></div> <div data-bind="template:{name:'cart-widget'}"></div> <button class="btn btn-primary btn-sm" data-bind="click:showOrder"> Confirm Order </button> </script>
It's important that you notice the template binding that we have just below <h1>Cart</h1>
. We are binding a template with an array using the foreach
argument. With this binding, Knockout renders the cart-item
template for each element inside the cart collection. This considerably reduces the code we write in each template and in addition makes them more readable.
We have once again used the cart-widget
template to show the total items and the total amount. This is one of the good features of templates, we can reuse content over and over.
Observe that we have put a button at the top-right of the cart to close it when we don't need to see the details of our cart, and the other one to confirm the order when we are done. The code in our view-model should be as follows:
var hideCartDetails = function () { $("#cartContainer").addClass("hidden"); }; var showOrder = function () { $("#catalogContainer").addClass("hidden"); $("#orderContainer").removeClass("hidden"); };
As you can see, to show and hide elements we use jQuery and CSS classes from the Bootstrap framework. The hidden class just adds the display: none
style to the elements. We just need to toggle this class to show or hide elements in our view. Expose these two methods in the return
statement of your view-model.
We will come back to this when we need to display the order
template.
This is the result once we have our catalog and our cart:

The order template
Once we have clicked on the Confirm Order button, the order should be shown to us, to review and confirm if we agree.
<script type="text/html" id="order"> <div class="col-xs-12"> <button class="btn btn-sm btn-primary" data-bind="click:showCatalog"> Back to catalog </button> <button class="btn btn-sm btn-primary" data-bind="click:finishOrder"> Buy & finish </button> </div> <div class="col-xs-6"> <table class="table"> <thead> <tr> <th>Name</th> <th>Price</th> <th>Units</th> <th>Subtotal</th> </tr> </thead> <tbody data-bind="foreach:cart"> <tr> <td data-bind="text:product.name"></td> <td data-bind="text:product.price"></td> <td data-bind="text:units"></td> <td data-bind="text:subtotal"></td> </tr> </tbody> <tfoot> <tr> <td colspan="3"></td> <td>Total:<span data-bind="text:grandTotal"></span></td> </tr> </tfoot> </table> </div> </script>
Here we have a read-only table with all cart lines and two buttons. One is to confirm, which will show the modal dialog saying the order is completed, and the other gives us the option to go back to the catalog and keep on shopping. There is some code we need to add to our view-model and expose to the user:
var showCatalog = function () { $("#catalogContainer").removeClass("hidden"); $("#orderContainer").addClass("hidden"); }; var finishOrder = function() { cart([]); hideCartDetails(); showCatalog(); $("#finishOrderModal").modal('show'); };
As we have done in previous methods, we add and remove the hidden class from the elements we want to show and hide. The finishOrder
method removes all the items of the cart because our order is complete; hides the cart and shows the catalog. It also displays a modal that gives confirmation to the user that the order is done.

Order details template
The finish-order-modal template
The last template is the modal that tells the user that the order is complete:
<script type="text/html" id="finish-order-modal"> <div class="modal fade" id="finishOrderModal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body"> <h2>Your order has been completed!</h2> </div> <div class="modal-footer"> <div class="form-group"> <div class="col-sm-12"> <button type="submit" class="btn btn-success" data-dismiss="modal">Continue Shopping </button> </div> </div> </div> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal --> </script>
The following screenshot displays the output:

- .NET之美:.NET關(guān)鍵技術(shù)深入解析
- OpenCV 3和Qt5計(jì)算機(jī)視覺應(yīng)用開發(fā)
- Python高效開發(fā)實(shí)戰(zhàn):Django、Tornado、Flask、Twisted(第2版)
- Scala謎題
- SQL Server 2016數(shù)據(jù)庫(kù)應(yīng)用與開發(fā)習(xí)題解答與上機(jī)指導(dǎo)
- Python深度學(xué)習(xí):基于TensorFlow
- Java圖像處理:基于OpenCV與JVM
- Apache Solr PHP Integration
- Software-Defined Networking with OpenFlow(Second Edition)
- Joomla!Search Engine Optimization
- Oracle SOA Suite 12c Administrator's Guide
- INSTANT PLC Programming with RSLogix 5000
- Getting Started with Windows Server Security
- 系統(tǒng)分析師UML用例實(shí)戰(zhàn)
- Building Microservices with .NET Core 2.0(Second Edition)