News

Create data-driven interfaces with KnockoutJS

Make a modular app that has real-time UI updates, multiple views, and is easily maintainable and extendable

knockout608

JavaScript developers have had it pretty good recently thanks to a proliferation of excellent MV* frameworks. One of the most popular is Knockout, the brainchild of Bristol-based Steve Sanderson.

Knockout utilises the MVVM pattern (Model-View-ViewModel) which sounds scarier than it is. It starts with a Model, which is your data; this is rendered in a view, your HTML; the View Model is the glue that binds data to your view, controlling what is seen and when.

MVVM encourages web developers to think more about how they structure their JS applications. This separation allows one of Knockout’s best features: automatic UI refresh; when data in your View Model changes the UI updates accordingly.
We’re going to dive into how to use the Knockout library with RequireJS to build a web interface. We’ll build an app that will list your films, get information about them from the Rotten Tomatoes API, and let you know where they physically are. We’ll also let users update the data from the view itself with inline editing.

Require and Bootstrap

Knockout is compatible with AMD (Asynchronous Module Definition) loaders like RequireJS. We’re using RequireJS to manage dependencies and load Knockout files. RequireJS looks for the data-main attribute and will load ‘js/app.js’, which we’ll create in a bit. We’re also using Bootstrap for some basic styles.

001
 002 < meta charset="”UTF-8”" />
 003 Film Finder
 004 < link href="”//netdna.bootstrapcdn.com/" rel="”stylesheet”" />
 005 < link href="”styles/aestheti.css”" rel="”stylesheet”" />
 006 < script type="text/javascript" src="”js/libs/require.js”" data-main="”js/app”">
 007

Require dependencies

To load our dependencies we give RequireJS an array of all the files (as strings) we’ll need for our app to run. DOMReady is a lightweight RequireJS plug-in to make sure that our callback is only run when the DOM is ready. filmViewModel is where our view model for the film logic will reside.

001 //app.js
 002 require([‘libs/knockout’, ‘filmViewModel’, ‘libs/jquery’, ‘libs/ domReady!’], function(ko, filmViewModel) {
 003 ‘use strict’;
 004 });

Film View Model

In total our app will contain three view models, one for the film section, one for the dashboard section, and one ‘master’ view model which will wrap the entire document. You cannot apply bindings multiple times to the same element so we need a master one which will allow both view models to access every single part of the page.

 001 define([‘libs/knockout’, ‘libs/jquery’], function (ko) {
 002 ‘use strict’;
 003 return function () {
 004 /* filmViewModel properties and methods */
 005 };
 006 });

Observables in VM

The crux of a Knockout view model are observables. Each property that we want to track we have to wrap in either ko.observableArray(), if we want to track an array, or ko.observable() for other types. We can pass a default value as an argument and we’ll see the relationship between this and our view.

001 var self = this;
 002 self.films = ko.observableArray(); //list of films
 003 self.currentFilm = ko.observable({}); //current film in view
 004 self.related = ko.observableArray(); //related films
 005 self.query = ko.observable(‘’); //search box value

Get data

Here is a basic PHP service which returns a list of JSON encoded films from a database. Each film has three properties: name, owner, and location. Once this data has been retrieved we pass the data to the ‘films’ observable which will automatically update the view (once we’ve initialised it).

001 //filmViewModel.js
 002 self.getFilms = function () {
 003 return $.getJSON(‘api.php’);
 004 };
 005 //app.js
 006 $.when(filmViewModel.getFilms()).then(function (films) {
 007 filmViewModel.films(films);
 008 });

Master View Model

To instantiate our film view model we call new filmViewModel(). We’ll then make this view model a child (property) of the master view model. We’ll also add an observable called ‘view’, which will dictate whether to show the film or dashboard. We’re also making a method that will switch between the different views, by reading the href attribute.

001 var filmViewModel = new filmViewModel();
 002 var appViewModel = {
 003 filmViewModel: filmViewModel,
 004 view: ko.observable(‘home’),
 005 switchView: function (model, event) {
 006 $(event.target).parent().siblings(). removeClass(‘active’);
 007 event.target.parentNode.classList.add(‘active’);
 008 appViewModel.view(event.target.getAttribute(‘href’). split(‘#’)[1]);
 009 }
 010 };

Apply bindings

To kickstart Knockout we have to marry together the HTML (view) with the view model. We do this by calling ko.applyBindings(), which takes a view model and an optional element to bind the view model to, if no element is provided then it’ll bind to the entire document. Remember, you can’t have multiple view models on the same element.

001 ko.applyBindings(appViewModel);
 002

Our view

So we’ve called applyBindings, but what are these bindings? We haven’t defined any yet! Knockout works by reading the contents of a data attribute called data-bind. As of Knockout 3.0 there are 22 types of bindings. In our navigation we’re using one, click, followed by the name of the method to call.

001 <div class=”page-header”>
 002    <h1>Film Finder</h1>
 003    <ul class=”nav nav-pills”>
 004        <li class=”active”>
 005            <a href=”#home” data-bind=”click: switchView”>Find     a film</a>
 006        </li>
 007        <li>
 008            <a href=”#dashboard” data-bind=”click:         switchView”>Dashboard</a>
 009        </li>
 010    </ul>
 011 </div>

Page structure

We’ll split the page into two sections, one that will use the filmViewModel and the second section that will use the dashboardViewModel. We can specify which View Model to use in a section with the ‘with’ binding. This means that we don’t have to specify filmViewModel.xyz every time as it specifies the scope for us.

001 <section data-bind=”with: filmViewModel”>
002    <!-- next step -->
003 </section>
004 <section data-bind=”with: dashboardViewModel”>
005 </section>


$root and $parent

Although we’ve just set our scope to filmViewModel we can still access properties of the master VM (view model) by using the $root keyword or the $parent keyword. We also have to invoke all observables to read their value (eg ‘view()’ instead of just ‘view’). They’re observables, no longer just properties, so they are to be treated as functions.

001 <div data-bind=”visible: $root.view() === ‘home’”>
002 </div>

Update values

We’ve got a list of our films, so now we’re just going to add a search box that will filter the list in real-time as the person types. This time we’re using a value binding, this is what is shown in the search box, and valueUpdate is which event to fire the update on the model.

001 <input type=”search” class=”form-control” data-bind=”value:     query, valueUpdate: ‘afterkeydown’” placeholder=”Search for a     film&hellip;”>
002 <button class=”btn btn-default” type=”button”>Find!</button>
003

Filter film array

Next we’re going to introduce some new concepts. A computed observable is one that watches one or more observables and returns something new when they change. Ours fires when the query observable changes. Knockout has a few utility methods, one being arrayFilter; it takes two arguments, the array and a matcher function that returns true or false.

001 self.filteredFilms = ko.computed(function() {
 002 var search = self.query().toLowerCase();
 003 return ko.utils.arrayFilter(self.films(), function(film) {
 004 return search.length ? film.title.toLowerCase(). indexOf(search) >= 0 : film.title;
 005 });
 006 }, self);

For each binding

To display our list of filtered films we’ll use another Knockout binding type, for each. This loops through an array and for each item in that array it’ll show the markup contained beneath. For each film we’ll add a click handler to get the film data from the Rotten Tomatoes API.

001 <div class=”row”>
002    <div class=“film-list-container“ data-bind=”foreach:     003    filteredFilms”>
004        <a href=”#” data-bind=”text: title, click: $parent.    getFilmData”></a>
005    </div>
006 </div>

Update observables

Knockout doesn’t require jQuery but we’ve included it to handle AJAX requests. When you click on one of the film titles, it passes the observable first and the event second. We’re also using jQuery’s extend() to merge the two objects so that the film data is merged with the owner and location data. We then pass this object to the currentFilm observable.

001 self.getFilmData = function (film) {
 002 $.getJSON(‘http://api.rottentomatoes.com/api/public/v1.0/ movies. json?q=’ + film.title + ‘&apikey=YOUR_API_KEY&callback=?’, function (data) {
 003 var movie = $.extend(film, data.movies[0]);
 004 self.currentFilm( movie );
 005 });
 006 };
 007 

Using observable values

Our data is in our view model but we’re not doing anything with it in the view yet. To display that data we can use the text binding. One downside of using a data binding library like Knockout is the amount of extra markup that’s sometimes needed, although Knockout’s solution to this is virtual elements.

001 <section class=”film-data”>
002    <div class=”page-header”>
003        <h2>
004            <span data-bind=”text: currentFilm().title”></        span>&nbsp;
005            <small data-bind=”text: currentFilm().year”></small>
006        </h2>
007    </div>
008 </section>

Attribute bindings

We’re using the ‘if’ binding to conditionally show the poster. We can set HTML attributes using the attr binding. This takes an object with the name of the attribute to set and what to set it to. Rotten Tomatoes sends us a ‘posters’ object with URIs pointing to different sizes – we’ll use the profile size one.

001 <div data-bind=”if: currentFilm().posters”>
002    <img data-bind=”attr: { src: currentFilm().posters.profile     }”     src=”” alt=“Film poster” class=“img-rounded”>
003 </div>

Virtual elements

We mentioned Knockout’s virtual elements to cut down on excess markup a little earlier on in Step 15. These are HTML comments with a special syntax, starting with ‘ko’. Virtual elements aren’t compatible with all binding types but it can cut down on extra tags. For example, here we just want a snippet of HTML to appear if it has a rating.

001 <!-- ko if: rating -->
002    <p>
003        Average rating:
004        <span data-bind=”text: rating”></span>%
005   </p>
006 <!-- /ko -->

Computed observables

The Rotten Tomatoes API returns an average critic rating and an average viewer rating. Here we’ll write another computed observable which will add both the ratings together and divide them by two in order to get the average for both of them. The second parameter for a computed observable is optional but sets the value for ‘this’.

001 self.rating = ko.computed(function () {
 002 if (this.currentFilm() && this.currentFilm().ratings) {
 003 return (this.currentFilm().ratings.audience_score + this. currentFilm().ratings.critics_score) / 2;
 004 }
 005 }, self);
 006 

Custom binding

We’ve made our own computed observables, next we’re going to step it up a notch and write our own custom bindings. This means that we can do more than data-bind=“click: x”, we could do data-bind=“edit: x”. We’re going to do just that and create an inline editor, credit to Craig Cavalier for this solution.

001 //based on http://jsfiddle.net/craigcav/MxU2V/
 002 ko.bindingHandlers.liveEditor = {
 003 //next step, init and update
 004 };

Init and update

All a custom binding needs to know is what to do with a property when it’s initialised and what to do when a value is updated. The valueAccessor function reads what the current value is and we’re also extending the observable with a new property, the editor. On update, it then updates the CSS class that has been applied to the element.

001 init: function (element, valueAccessor) {
 002 var observable = valueAccessor();
 003 observable.extend({ liveEditor: this });
 004 },
 005 update: function (element, valueAccessor) {
 006 var observable = valueAccessor();
 007 ko.bindingHandlers.css.update(element, function () { 
 008 return { editing: observable.editing }; 
 009 });
 010 }

Extend observables

In order to extend an observable we need to register it with ko.extenders. When we click on an editable element it’s replaced with an input box. All observables that are under the liveEditor binding will inherit these properties so we could have many bits of text and corresponding input boxes all keeping in sync automatically.

001 ko.extenders.liveEditor = function (target) {
 002 target.editing = ko.observable(false);
 003 target.edit = function () {
 004 target.editing(true);
 005 };
 006 target.stopEditing = function () {
 007 target.editing(false);
 008 };
 009 return target;
 010 };

Live edit view

To use this live-editing feature we have a click handler on the location, by calling edit it sets ‘editing’ to true. This in turn shows the input box by adding a class of ‘editing’ to the surrounding element. We then have some simple CSS rules that hide and show the input box depending on the mode.

001 <div data-bind=”if: currentFilm().location”>
002    <div data-bind=”liveEditor: currentFilm().location”>
003        <span class=”view”>
004            Location: <a href=”#” data-bind=”click:         currentFilm().    location.edit, text: currentFilm().location()”></a>
005        </span>
006    </div>
007 </div>

Editing values

The input itself displays the value of the location observable. The editing state is started when the input element has focus. When the Enter key is pressed or the user clicks away (the blur event), then stopEditing is called and this reverts it back to text with the updated value.

< input type="text" data-bind="”value:" />
001 <input class=”edit” data-bind=”value: currentFilm().location,     enterKey: currentFilm().location.stopEditing, selectAndFocus:     currentFilm().location.editing, event: { blur: currentFilm().    location.stopEditing }”>

Create Dashboard VM

So far we’ve only concentrated on the film view model, so let’s turn our attention onto the Dashboard view. This’ll show statistics on our film collection, like ownership and average overall ratings. Similar to our other view model, we’ll define it and let RequireJS know what other libraries this depends on. We’ll be using Chart.js (www.chartjs.org) to visualise it.

001 define([‘libs/knockout’, ‘libs/chart’ ,’libs/jquery’], function(ko) {
 002 ‘use strict’;
 003 return function () {
 004 var self = this;
 005 /* next step */
 006 }
 007 });

Create fading binding

Jumping back to app.js, we’ll create another custom binding to replace the visible binding. Instead of just appearing and disappearing, we’ll use jQuery’s fadeIn() and fadeOut() functions to toggle between the elements. unwrapObservable passes back the value of an observable if it is an observable or just the plain property if it isn’t.

001 ko.bindingHandlers.fadeVisible = {
 002 init: function(element, valueAccessor) {
 003 var observable = valueAccessor();
 004 $(element).toggle(ko.utils. unwrapObservable(observable));
 005 },
 006 update: function(element, valueAccessor) {
 007 var observable = valueAccessor();
 008 ko.utils.unwrapObservable(observable) ? $(element). fadeIn() : $(element).fadeOut();
 009 }
 010 };

Use fadeVisible

To use our fadeVisible binding, all we have to do is replace visible with fadeVisible and our different sections will fade between each other! It’s still triggered by the same condition (if the dashboard is active) but there should now be a smooth transition between each view. We’ve also made a canvas element for the chart as well.

001 <div data-bind=”fadeVisible: $root.view() === ‘dashboard’”>
002    <h2>Dashboard</h2>
003    <h3>Possession</h3>
004    <canvas id=”possession-chart” height=”250” width=”250”></    canvas>
005    <h3>Average Rating:</h3>
006 </div>

Add Dashboard

The final step is instantiating and adding our third view model to the master view model. This makes all of its properties and methods available to the view. Further on down this file we’ve already called applyBindings so now it’ll include the dashboard view model bindings as well. You could go on and on like this, keeping view models separated.

001 var dashboardViewModel = new dashboardViewModel();
 002 var appViewModel = {
 003 /* existing properties and methods */
 004 dashboardViewModel: dashboardViewModel
 005 };
 006 

Is Knockout a knockout

Knockout can initially be a little overwhelming with its confusing terminology and having to wrap everything in special functions. However, once you understand the implications of doing it and how to access the values afterwards, it becomes a lot clearer. We’ve seen how powerful it is keeping dependency chains updated instantaneously. If you’ve got a complex app with DOM manipulation that could be solved by Knockout, then we highly recommend that you give it a chance.

×