In this article by Jorge Ferrando, author of the book KnockoutJS Essentials, we are going talk about how to design our templates with the native engine and then we will speak about mechanisms and external libraries we can use to improve the Knockout template engine.
When our code begins to grow, it's necessary to split it in several parts to keep it maintainable. When we split JavaScript code, we are talking about modules, classes, function, libraries, and so on. When we talk about HTML, we call these parts templates.
KnockoutJS has a native template engine that we can use to manage our HTML. It is very simple, but also has a big inconvenience: templates, it should be loaded in the current HTML page. This is not a problem if our app is small, but it could be a problem if our application begins to need more and more templates.
(For more resources related to this topic, see here.)
First of all, we are going to add some style to the page. Add a file called style.css into the css folder. Add a reference in the index.html file, just below the bootstrap reference. The following is the content of the file:
.container-fluid { margin-top: 20px; } .row { margin-bottom: 20px; } .cart-unit { width: 80px; } .btn-xs { font-size:8px; } .list-group-item { overflow: hidden; } .list-group-item h4 { float:left; width: 100px; } .list-group-item .input-group-addon { padding: 0; } .btn-group-vertical > .btn-default { border-color: transparent; } .form-control[disabled], .form-control[readonly] { background-color: transparent !important; }
Now remove all the content from the body tag except for the script tags and paste in these lines:
<div class="container-fluid"> <div class="row" id="catalogContainer"> <div class="col-xs-12" data-bind="template:{name:'header'}"></div> <div class="col-xs-6" data-bind="template:{name:'catalog'}"></div> <div id="cartContainer" class="col-xs-6 well hidden" data-bind="template:{name:'cart'}"></div> </div> <div class="row hidden" id="orderContainer" data-bind="template:{name:'order'}"> </div> <div data-bind="template: {name:'add-to-catalog-modal'}"></div> <div data-bind="template: {name:'finish-order-modal'}"></div> </div>
Let's review this code.
We have two row classes. They will be our containers.
The first container is named with the id value as catalogContainer and it will contain the catalog view and the cart. The second one is referenced by the id value as orderContainer and we will set our final order there.
We also have two more <div> tags at the bottom that will contain the modal dialogs to show the form to add products to our catalog and the other one will contain a modal message to tell the user that our order is finished.
Along with this code you can see a template binding inside the data-bind attribute. This is the binding that Knockout uses to bind templates to the element. It contains a name parameter that represents the ID of a template.
<div class="col-xs-12" data-bind="template:{name:'header'}"></div>
In this example, this <div> element will contain the HTML that is inside the <script> tag with the ID header.
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 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.
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.
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 { searchTerm: searchTerm, catalog: filteredCatalog, newProduct: newProduct, totalItems:totalItems, addProduct: addProduct, cart: cart, showCartDetails: showCartDetails, };
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>
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 viewmodel, 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 viewmodel, 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.
This is a very simple template. We just wrap the code 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>
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 viewmodel:
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 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 viewmodel 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 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 viewmodel 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:
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 viewmodel 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 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:
You have learned how to show and hide templates with the power of jQuery and Bootstrap. This is quite good because you can use this technique with any framework you want. The problem with this type of code is that since jQuery is a DOM manipulation library, you need to reference elements to manipulate them. This means you need to know over which element you want to apply the action. Knockout gives us some bindings to hide and show elements depending on the values of our view-model. Let's update the show and hide methods and the templates.
Add both the control variables to your viewmodel and expose them in the return statement.
var visibleCatalog = ko.observable(true); var visibleCart = ko.observable(false);
Now update the show and hide methods:
var showCartDetails = function () { if (cart().length > 0) { visibleCart(true); } }; var hideCartDetails = function () { visibleCart(false); }; var showOrder = function () { visibleCatalog(false); }; var showCatalog = function () { visibleCatalog(true); };
We can appreciate how the code becomes more readable and meaningful. Now, update the cart template, the catalog template, and the order template.
In index.html, consider this line:
<div class="row" id="catalogContainer">
Replace it with the following line:
<div class="row" data-bind="if: visibleCatalog">
Then consider the following line:
<div id="cartContainer" class="col-xs-6 well hidden" data-bind="template:{name:'cart'}"></div>
Replace it with this one:
<div class="col-xs-6" data-bind="if: visibleCart"> <div class="well" data-bind="template:{name:'cart'}"></div> </div>
It is important to know that the if binding and the template binding can't share the same data-bind attribute. This is why we go from one element to two nested elements in this template. In other words, this example is not allowed:
<div class="col-xs-6" data-bind="if:visibleCart, template:{name:'cart'}"></div>
Finally, consider this line:
<div class="row hidden" id="orderContainer" data-bind="template:{name:'order'}">
Replace it with this one:
<div class="row" data-bind="ifnot: visibleCatalog"> <div data-bind="template:{name:'order'}"></div> </div>
With the changes we have made, showing or hiding elements now depends on our data and not on our CSS. This is much better because now we can show and hide any element we want using the if and ifnot binding.
Let's review, roughly speaking, how our files are now:
We have our index.html file that has the main container, templates, and libraries:
<!DOCTYPE html> <html> <head> <title>KO Shopping Cart</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="css/style.css"> </head> <body> <div class="container-fluid"> <div class="row" data-bind="if: visibleCatalog"> <div class="col-xs-12" data-bind="template:{name:'header'}"></div> <div class="col-xs-6" data-bind="template:{name:'catalog'}"></div> <div class="col-xs-6" data-bind="if: visibleCart"> <div class="well" data-bind="template:{name:'cart'}"></div> </div> </div> <div class="row" data-bind="ifnot: visibleCatalog"> <div data-bind="template:{name:'order'}"></div> </div> <div data-bind="template: {name:'add-to-catalog-modal'}"></div> <div data-bind="template: {name:'finish-order-modal'}"></div> </div> <!-- 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> <!-- libraries --> <script type="text/javascript" src="js/vendors/jquery.min.js"></script> <script type="text/javascript" src="js/vendors/bootstrap.min.js"></script> <script type="text/javascript" src="js/vendors/knockout.debug.js"></script> <script type="text/javascript" src="js/models/product.js"></script> <script type="text/javascript" src="js/models/cartProduct.js"></script> <script type="text/javascript" src="js/viewmodel.js"></script> </body> </html>
We also have our viewmodel.js file:
var vm = (function () { "use strict"; var visibleCatalog = ko.observable(true); var visibleCart = ko.observable(false); var catalog = ko.observableArray([...]); var cart = ko.observableArray([]); var newProduct = {...}; var totalItems = ko.computed(function(){...}); var grandTotal = ko.computed(function(){...}); var searchTerm = ko.observable(""); var filteredCatalog = ko.computed(function () {...}); var addProduct = function (data) {...}; var addToCart = function(data) {...}; var removeFromCart = function (data) {...}; var showCartDetails = function () {...}; var hideCartDetails = function () {...}; var showOrder = function () {...}; var showCatalog = function () {...}; var finishOrder = function() {...}; return { searchTerm: searchTerm, catalog: filteredCatalog, cart: cart, newProduct: newProduct, totalItems:totalItems, grandTotal:grandTotal, addProduct: addProduct, addToCart: addToCart, removeFromCart:removeFromCart, visibleCatalog: visibleCatalog, visibleCart: visibleCart, showCartDetails: showCartDetails, hideCartDetails: hideCartDetails, showOrder: showOrder, showCatalog: showCatalog, finishOrder: finishOrder }; })(); ko.applyBindings(vm);
It is useful to debug to globalize the view-model. It is not good practice in production environments, but it is good when you are debugging your application.
Window.vm = vm;
Now you have easy access to your view-model from the browser debugger or from your IDE debugger.
In addition to the product model, we have created a new model called CartProduct:
var CartProduct = function (product, units) { "use strict"; var _product = product, _units = ko.observable(units); var subtotal = ko.computed(function(){...}); var addUnit = function () {...}; var removeUnit = function () {...}; return { product: _product, units: _units, subtotal: subtotal, addUnit : addUnit, removeUnit: removeUnit }; };
You have learned how to manage templates with Knockout, but maybe you have noticed that having all templates in the index.html file is not the best approach. We are going to talk about two mechanisms. The first one is more home-made and the second one is an external library used by lots of Knockout developers, created by Jim Cowart, called Knockout.js-External-Template-Engine (https://github.com/ifandelse/Knockout.js-External-Template-Engine).
Since we want to load templates from different files, let's move all our templates to a folder called views and make one file per template. Each file will have the same name the template has as an ID. So if the template has the ID, cart-item, the file should be called cart-item.html and will contain the full cart-item template:
<script type="text/html" id="cart-item"></script>
The views folder with all templates
Now in the viewmodel.js file, remove the last line (ko.applyBindings(vm)) and add this code:
var templates = [ 'header', 'catalog', 'cart', 'cart-item', 'cart-widget', 'order', 'add-to-catalog-modal', 'finish-order-modal' ]; var busy = templates.length; templates.forEach(function(tpl){ "use strict"; $.get('views/'+ tpl + '.html').then(function(data){ $('body').append(data); busy--; if (!busy) { ko.applyBindings(vm); } }); });
This code gets all the templates we need and appends them to the body. Once all the templates are loaded, we call the applyBindings method. We should do it this way because we are loading templates asynchronously and we need to make sure that we bind our view-model when all templates are loaded.
This is good enough to make our code more maintainable and readable, but is still problematic if we need to handle lots of templates. Further more, if we have nested folders, it becomes a headache listing all our templates in one array. There should be a better approach.
We have seen two ways of loading templates, both of them are good enough to manage a low number of templates, but when lines of code begin to grow, we need something that allows us to forget about template management. We just want to call a template and get the content.
For this purpose, Jim Cowart's library, koExternalTemplateEngine, is perfect. This project was abandoned by the author in 2014, but it is still a good library that we can use when we develop simple projects. We just need to download the library in the js/vendors folder and then link it in our index.html file just below the Knockout library.
<script type="text/javascript" src="js/vendors/knockout.debug.js"></script> <script type="text/javascript" src="js/vendors/koExternalTemplateEngine_all.min.js"></script>
Now you should configure it in the viewmodel.js file. Remove the templates array and the foreach statement, and add these three lines of code:
infuser.defaults.templateSuffix = ".html"; infuser.defaults.templateUrl = "views"; ko.applyBindings(vm);
Here, infuser is a global variable that we use to configure the template engine. We should indicate which suffix will have our templates and in which folder they will be.
We don't need the <script type="text/html" id="template-id"></script> tags any more, so we should remove them from each file.
So now everything should be working, and the code we needed to succeed was not much.
KnockoutJS has its own template engine, but you can see that adding new ones is not difficult. If you have experience with other template engines such as jQuery Templates, Underscore, or Handlebars, just load them in your index.html file and use them, there is no problem with that. This is why Knockout is beautiful, you can use any tool you like with it.
You have learned a lot of things in this article, haven't you?
In this article, you have learned how to split an application using templates that share the same view-model. Now that we know the basics, it would be interesting to extend the application. Maybe we can try to create a detailed view of the product, or maybe we can give the user the option to register where to send the order.
Further resources on this subject: