Keeping track of UI state in web applications can get very complicated very quickly. Data is coming and going; the user is opening and closing components, selecting, filtering and rearranging; and just relying on DOM inspection to understand where things are is likely to end in frustration. Trust me, I’ve been there.
Holding data and state in a separate layer to the interface makes rich UI development a lot easier – not to mention much more robust – and the Knockout library is a great way to do it. Having used it throughout 2011 it’s my pick for the most useful web technology of the year, one of those things you don’t know how you lived without!
What is it?
Knockout (http://knockoutjs.com) is a JavaScript implementation of the Model-View-View Model pattern, which may be familiar if you’ve worked with Silverlight or WPF, or if you’ve used MVC practices. It’s a similar concept with a more declarative syntax. For the rest of us, think of it as a way to define data in a model object and then bind DOM elements or templates to that data.
Much clearer? Not really? Okay, here’s a bit more detail…
The Model part is your data. Typically this will come in the form of JSON loaded via AJAX, but there are plenty of other ways to get data into your app.
The View part is your HTML, with any element you want to populate or manipulate given a binding with a “data-bind” attribute. This is using the HTML5 custom data-* attribute syntax so it passes validation, and can be interpreted in HTML4 and XHTML documents as well.
Finally the View Model is the JavaScript object instance which connects everything together. These are reusable functions, so you can have multiple instances of a View Model in one page, or nest a child model in a parent.
Whenever the View Model changes – either through a data load or user action – the appropriate data-bound DOM elements are automatically updated so that the UI is always in sync with the View Model – easy! Bindings can even be two-way, so a value binding on a form element will update the JavaScript model object on user input, ready to be saved back to the server.
The documentation and interactive tutorials on the Knockout site are excellent, so rather than try to repeat them I would recommend you take a look and work through them to get a feel for what it can do. I’ve got plenty of leftover turkey to consume, so take your time.
Putting it to use
A simple example of using a View Model to track and update UI state is an image gallery. We have data, in the set of images and captions to display, and state, in which image is selected and other parameters such as whether the thumbnail area should page left and right and whether we’re at the start or end of the paging. This will be a fairly bare bones example but you should see that there’s a load of potential to extend it.
Getting started
The underlying principle when developing with a View Model is that it doesn’t have (and doesn’t need) any knowledge of how the DOM is structured or laid out. As long as your HTML has the appropriate data bindings it will all work regardless of the way it looks. All the View Model is dealing with is data, and this loose coupling means it’s really easy to build logical, testable components you can fit together any way you like. It’s outside the scope of this article, but the full set of unit tests used in developing the demo is included in the Github repo.
I’ve also tried to keep the demo framework-agnostic where possible. I’m using jQuery only to initialise the page and View Model but there’s no reason you couldn’t replace this with your framework of choice or just pure JavaScript.
So let’s look at the parts which make up the demo…
First is the data, or Model, which in this case comes from a list of links to images in an HTML document. From here we can extract the URL to each image and its related caption and supply them to the View Model’s init method. This is then hidden by JavaScript, so that the basic image list would still be available to clients that can’t apply the richer UI.
<ul class="origin">
<li><a href="img/1.jpg">Image 1 Caption</a></li>
<li><a href="img/2.jpg">Image 2 Caption</a></li>
<li><a href="img/3.jpg">Image 3 Caption</a></li>
<li><a href="img/4.jpg">Image 4 Caption</a></li>
<li><a href="img/5.jpg">Image 5 Caption</a></li>
<li><a href="img/6.jpg">Image 6 Caption</a></li>
<li><a href="img/7.jpg">Image 7 Caption</a></li>
<li><a href="img/8.jpg">Image 8 Caption</a></li>
</ul>
Next we’ll create a simple View Model which holds the data and which image is selected, and also deals with what happens if the user changes the selection.
If you look at the project structure you’ll see I’ve created a file – gallery.js – to hold my View Models and keep them separate from other site code. On a larger project I would split this down further into a file for each model but things aren’t too complicated here.
Habitually I create a namespace for my code, although what you call this or whether you use one at all is up to you. Just remember that it greatly reduces the chances of conflicting with any other JavaScript in your site, and we have the freedom to call our gallery View Model ‘gallery’ without worrying that there might be another ‘gallery’ defined somewhere else.
var site = site || { models: { } };
Here we’re testing whether site has already been defined in another file, and if not create a simple object structure inside it. Once this is set up we’ll make a simple View Model within the object.
site.models.Gallery = function() {
this.itemsObservables = ko.observableArray();
}
Inside the View Model is a Knockout observable, itemsObservables, which is where we’ll store the data for our gallery: the image URLs and captions. It’s an observable array, which means that when we push or remove items to it Knockout can track this and update the UI accordingly.
Creating it on this makes it a property of the function object, so that when we instantiate a copy of the View Model later on this observable will be available as a public method.
This is great, but now we need to actually get the data into the View Model. By setting up ko.observable or ko.observableArray with an empty function call we are creating them with ‘undefined’ contents so we should create an initialisation method to put something inside them.
site.models.Gallery = function() {
var self = this;
this.itemsObservables = ko.observableArray();
this.init = function(data) {
ko.utils.arrayForEach(data,function(item) {
self.itemsObservables.push(new site.models.GalleryItem(item));
});
}
}
Inside the Gallery View Model we have added a second item, init, which is a function that takes a data array; in our case it’s going to be the result of a query on the DOM. Again it’s defined as a public method, within this, so that we can call it from outside the View Model.
The body of the function uses a Knockout utility method, arrayForEach, to loop through the data and push each item into the observable array we defined earlier. You could also use $.each in jQuery or _.each in Underscore, or any other method you like. You’ll also see we’ve created a variable self, which we use inside the arrayForEach callback to refer to the View Model’s overall scope – the callback function having its own scope.
But wait; take a look at what we’re actually pushing into the observable array.
ko.utils.arrayForEach(data,function(item) {
self.itemsObservables.push(new site.models.GalleryItem(item));
});
Rather than just pushing in the item itself, which is going to be a DOM element, we’re creating an instance of a further View Model, GalleryItem, which can then hold its own properties and observables. The new keyword creates a fresh copy of the GalleryItem object, so we can use as many of them as we need to – and they all have their own separate little chunk of data. This child View Model is really simple, but also really useful.
site.models.GalleryItem = function(el) {
this.isSelected = ko.observable(false);
this.src = el.href;
this.caption = el.innerHTML;
}
First we create a Knockout observable isSelected which (as may be obvious) is whether or not this item is selected. We default this to false by passing in the value when we create the observable.
Then (and here we are relying on having an ‘a’ element passed in but you could test for others if required) we set this.src to the element’s href attribute and this.caption to its innerHTML. These are simple variables, rather than observables, because we’re not expecting them to change and therefore don’t need the overhead of keeping them in Knockout’s observable chain. The reason we’re doing this at all is that we’re extracting the data from the element and storing it in an abstract object so we can apply it again any way we like. See, useful!
Once the View Model is in a state we can use, we can look at the HTML for the gallery UI, or View, where we will data-bind its observables.
<div class="gallery" data-bind="foreach: itemsObservables">
<div class="item"><img width="800" height="533" data-bind="attr: { 'src': src, 'alt': caption }" /></div>
</div>
You can see that we’ve set up a container element, div.gallery, and within this is a template, div.item. On the container is a data-bind attribute with a value of foreach: itemsObservables, which is telling Knockout to loop through that observable array and apply the template to its items. The items are the instances of the GalleryItem View Model we created in the init function, so the data binding on the image element within the template can access the src and caption values inside each one and set the element attributes accordingly.
The last step to make all this work is to create an instance of the Gallery View Model and populate it with our DOM element array. I’m using jQuery in a ready function for this but feel free to substitute your library and technique of choice.
$(function() {
var viewModel = new site.models.Gallery();
viewModel.init($('ul.origin a'));
ko.applyBindings(viewModel,$('body').get(0));
});
Pretty straightforward here, I create a variable viewModel which is a new copy of the Gallery View Model, and then call its init method, passing in the result of a DOM query for all the links within our list of items.
The final line is the Knockout method which applies the data in the View Model to all the bindings in our templates. In this case, it’s applying to the body element but you can target anywhere in the page DOM if you want to restrict the bindings just to that part. Once this is done, any changes to the View Model will be instantly reflected in the UI and vice-versa.
Moving on
If you were to run up all this code you would notice that it isn’t very “gallery” like, as all that the template does is to loop through the items and display their images. We still need a way for one image to be selected / displayed at a time; for the user to be able to see which in the list is selected; and to be able to navigate (i.e. to change the selection).
Fortunately with Knockout, this is really simple.
If you recall, when we set up the GalleryItem View Model we included an observable property isSelected. We can assign this true or false to indicate which the selected item in the set is, then use it as a binding in the template to show only that item.
<div class="gallery" data-bind="foreach: itemsObservables">
<div class="item" data-bind="visible: isSelected">
<img width="800" height="533" data-bind="attr: { 'src': src, 'alt': caption }" />
</div>
</div>
Now, as well as the attribute bindings on the image, we are also binding ‘visible’ to isSelected on div.item. So any item where isSelected is false will have its CSS display rule set to ‘none’, and as the View Model changes so will the visibility of the items.
We’re setting isSelected to false by default when we create our GalleryItem instances, so in the main View Model init method let’s also set isSelected on an item, the first, to true so that it will display.
this.init = function(data) {
var self = this;
ko.utils.arrayForEach(data,function(item) {
self.itemsObservables.push(new site.models.GalleryItem(item));
});
this.itemsObservables()[0].isSelected(true);
}
As well using Knockout’s internal methods (like ‘push’, which is its own rather than the pure JavaScript ‘push’) on the itemsObservables array, we can also call it with () and access any of its items. We assign values to regular observables by calling them with the value as the argument, so the new line in the init function now sets isSelected in the first item in the observable array to true.
All good so far, but let’s also create a strip of thumbnails which will show the user a preview of all the images and which one is currently selected. So back in the HTML, add a new template.
<div class="controller">
<ul data-bind="foreach: itemsObservables">
<li data-bind="css: { 'selected': isSelected }">
<img data-bind="attr: { 'src': src}" width="140" />
<span data-bind="text: caption"></span>
</li>
</ul>
</div>
We’re using the same foreach Knockout method to display as many list items as there are items in the observable array, but rather than using isSelected to show and hide we’re using it to conditionally apply a class to the list item which has the selection. You can style this however you like, but I’ve given it a simple yellow background for the demo. I’m also using a squashed version of the main image as the thumbnail for ease, but I would expect a production site to have right-sized thumbnails.
So with something looking a bit more like a gallery, we can now give the user a way to navigate between images. Clicking or tapping on the thumbnail is a pretty good way to do this, and we can use Knockout to handle the click event and make the appropriate selection.
<div class="controller">
<ul data-bind="foreach: itemsObservables">
<li data-bind="css: { 'selected': isSelected }, click: $parent.select">...
Now in the controller template we’re extending the data binding on the list item to assign the function $parent.select to the click event. Knockout will take over the default DOM event and any event listeners already on this element, but you can pass control back to them later if you need to by returning true from the function we’re about to create. The $parent prefix of the function assignment is there because, on the item, we’re in the context of the GalleryItem View Model, and by using it we can access its parent View Model, the instance of Gallery, and call a function select which we’ll define there.
this.select = function(e) {
var newSelection = ko.dataFor(e.target);
self.setSelected(newSelection);
e.preventDefault();
}
this.setSelected = function(newSelection) {
ko.utils.arrayForEach(self.itemsObservables(),function(item) {
item.isSelected(item == newSelection);
});
}
Actually there are two new functions here: select which handles the click event and then calls setSelected, which makes the selection. It’s not essential to split things up this way, but by creating a separate setSelected method we can test it independently without needing to simulate a DOM event.
Inside select we’re finding out the View Model’s context from the item we’ve clicked on. We can do this easily using the Knockout utility method ko.dataFor which will trace back what data the event target node is bound to. The function then calls setSelected with this data and prevents the default action. As we clicked on a list item in this example there’s no default action, so it’s not strictly necessary, but if in the future we changed the template to be a link instead it won’t catch us out.
We could simply set isSelected on the new selection to true, which will update the UI instantly. However, this would cause a problem in that the previous selection would still be active and there would then be two large images shown and two selections in the thumbnails.
To prevent this we loop through all the items in the itemsObservable and compare them to the new selection. If it’s a match we set isSelected to true, otherwise it becomes false. We don’t even need to set a value explicitly because the result of the comparison itself will be a Boolean – which is all we need. In this way only one selection can be made at a time.
Wrapping up
By now we have a minimal, but functional, image gallery. Only one image from a set is shown at once, and there’s a display of thumbnails that the user can click on to select which is displayed. Throughout everything we’ve maintained a separation of UI and logic, so we could totally replace the view – as long as all the bindings were transferred – and everything would still work.
There are of course countless, significantly more feature-rich, gallery plugins out there for any JavaScript framework you’d care to mention. For me the advantage of doing this kind of thing in Knockout is that you end up with a compact (52 lines commented and uncompressed) yet extensible controller in the form of the View Model which can be applied to any UI without having to worry about how nicely it will play with your existing page.
I had a lot more to add – I haven’t even touched on paging in the thumbnail area yet, let alone all the other ways you can extend Knockout with richer UI effects, but I sense that this article is long enough already for the post-Christmas attention span. As well as the master branch in my Github repo (http://github.com/fulljames/12devs) there is an extended branch, if you wanted to look ahead.
Meanwhile I will endeavour to get more on this subject written up and posted somewhere – hopefully the 12 Devs editors will consider linking to it in due course.
Are there any mince pies left? Cheers!