Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Are Your Templates Working Overtime?

| Comments

Update: much of this is obsolete, if using native templates in KO 2.0

When reading the KnockoutJS documentation regarding observableArrays, one feature that immediately stands out is the ability to have a template rendered only for the changed items in an array. This is accomplished by using the foreach option of the template binding, as opposed to the {{each}} syntax of the jQuery Templates plugin.

The Problem

Anyone using an observableArray that is frequently manipulated will certainly want to take advantage of the foreach feature. However, I find it easy to catch your fingers typing something like this:

1
2
3
4
5
6
7
8
9
<script id="main" type="text/html">
    {{if items() && items().length > 0}}
    <div class="items">
         <h3>Items</h3>
        <ul data-bind="template: { name: 'itemTmpl', foreach: items }"></ul>
     </div>
    {{/if}}
    <a href data-bind="click: addItem">Add Item</a>
</script>

Our objective is simple: If there is anything in our items array, then we want to display a section with a header and the name of each item. Whenever an item is added to our array, we want to just render that new item using a template named itemTmpl. The foreach option of the template binding should take care of this for us, but we have a problem. All of our items are actually getting re-rendered on each change. Try it here.

Dependencies in Templates

In Knockout, bindings are implemented using computed observables to keep track of any dependencies that should trigger the binding to run again. The template binding uses this concept as well. Any observables that are accessed in your template outside of data-bind attributes will create a dependency.

Our main template actually has a dependency on the items observableArray in the {{if}} statement that checks to see if the array exists and has a length greater than zero. Any change to items will cause this template to be re-rendered, which will re-render the ul element that contains our foreach.

Potential Solutions

To find a potential solution to this issue, we need to be very careful about which observables that we access outside of a data-bind attribute in our template. The easiest solution is to use the visible binding on our section container instead of wrapping it in an {{if}} statement. If we know that our observableArray will always be initailized to at least an empty array, then the we won’t need the null check. In many cases, this should be a suitable alternative.

1
2
3
4
5
6
7
<script id="main" type="text/html">
     <div data-bind="visible: items().length > 0" class="items">
         <h3>Items</h3>
         <ul data-bind="template: { name: 'itemTmpl', foreach: items }"></ul>
     </div>
     <a href data-bind="click: addItem">Add Item</a>
</script>

On each change of the items array, we will now only evaluate the visibility of the container div rather than re-render the entire template.

In some cases though, the {{if}} statement may really have been our preferred approach. Maybe we have to do a lot of unnecessary work in the template to generate some elements that will be hidden by the visible binding. What we would really like to do is bind to an observable that only changes when the array crosses between 0 and 1 items.

The easiest way to accomplish this is by using a manual subscription. A computed observable seems like a logical choice, but it would actually get re-evaluated on any change to its dependencies. We only want to trigger a change when the array moves between empty and having items. A manual subscription that controls a separate observable will give us the flexibility to control how it changes.

1
2
3
4
5
6
7
8
9
10
viewModel.hasItems = ko.observable(true);

//only change hasItems when we cross between 0 and 1 items
viewModel.items.subscribe(function() {
    var current = viewModel.hasItems();
    var actual = viewModel.items() && viewModel.items().length > 0;
    if (current != actual) {
        viewModel.hasItems(actual);
    }
}, viewModel);

Now, we can go back to using a template with an {{if}} statement, but this time it will be bound to the hasItems observable.

1
2
3
4
5
6
7
<script id="main" type="text/html">
     <div class="items">
         <h3>Items</h3>
         <ul data-bind="template: { name: 'itemTmpl', foreach: items }"></ul>
     </div>
     <a href data-bind="click: addItem">Add Item</a>
</script>

The template rendering is smart enough to recognize that hasItems by itself is a function and will call it appropriately to retrieve the value of our observable, so we do not need to write hasItems(). In this case we want our template to get re-rendered whenever hasItems changes. However, if our main template had more content, then this might not be so attractive. We might need to break our templates into smaller parts and pass either the data or foreach options to the template binding to render sub-templates.

Test your Templates

For any application that makes significant use of the foreac option of the template binding, it would be useful to at least do a small amount of testing to ensure that your templates are being rendered as you expect.

One easy way to do this testing is to print out the time that your template is being rendered. The way that I have been doing this is by extending the jQuery Template plugin tags with a now tag like this:

1
2
3
4
5
$.extend(jQuery.tmpl.tag, {
    now: {
        open: '__=__.concat((" -rendered at: " + (new Date()).toLocaleTimeString()));'
    },
});

You can use anywhere that you want this information included. Note: if you are using a version of the jQuery Templates plugin prior to 1.0.0pre, then you should use _ instead of __ in the open tag.

Sample:

Link to full sample on jsFiddle.net

Comments