Advertisement

Building Large, Maintainable, and Testable Knockout.js Applications

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

Knockout.js is a popular open source (MIT) MVVM JavaScript framework, created by Steve Sandersen. Its website provides great information and demos on how to build simple applications, but it unfortunately doesn't do so for larger applications. Let's fill in some of those gaps!


AMD and Require.js

AMD is a JavaScript module format, and one of the most popular (if not the most) frameworks is http://requirejs.org by https://twitter.com/jrburke. It consists of two global functions called require() and define(), although require.js also incorporates a starting JavaScript file, such as main.js.

<script src="js/require-jquery.min.js" data-main="js/main"></script>

There are primarily two flavors of require.js: a vanilla require.js file and one that includes jQuery (require-jquery). Naturally, the latter is used predominately in jQuery-enabled websites. After adding one of these files to your page, you can then add the following code to your main.js file:

require( [ "https://twitter.com/jrburkeapp" ], function( App ) {
    App.init();
})

The require() function is typically used in the main.js file, but you can use it to directly include a module anywhere. It accepts two arguments: a list of dependencies and a callback function.

The callback function executes when all dependencies finish loading, and the arguments passed to the callback function are the objects required in the aforementioned array.

It's important to note that the dependencies load asynchronously. Not all libraries are AMD compliant, but require.js provides a mechanism to shim those types of libraries so that they can be loaded.

This code requires a module called app, which could look like the following:

define( [ "jquery", "ko" ], function( $, ko ) {
    var App = function(){};

    App.prototype.init = function() {
        // INIT ALL TEH THINGS
    };

    return new App();
});

The define() function's purpose is to define a module. It accepts three arguments: the name of the module (which is typically not included), a list of dependencies and a callback function. The define() function allows you to separate an application into many modules, each having a specific function. This promotes decoupling and separation of concerns because each module has its own set of specific responsibilities.

Using Knockout.js and Require.js Together

Knockout is AMD ready, and it defines itself as an anonymous module. You don't need to shim it; just include it in your paths. Most AMD-ready Knockout plugins list it as "knockout" rather than "ko", but you can use either value:

require.config({
    paths: {
        ko: "vendor/knockout-min",
        postal: "vendor/postal",
        underscore: "vendor/underscore-min",
        amplify: "vendor/amplify"
    },
    shim: {
        underscore: {
            exports: "_"
        },
        amplify: {
            exports: "amplify"
        }
    },
    baseUrl: "/js"
});

This code goes at the top of main.js. The paths option defines a map of common modules that load with a key name as opposed to using the entire file name.

The shim option uses a key defined in paths and can have two special keys called exports and deps. The exports key defines what the shimmed module returns, and deps defines other modules that the shimmed module might depend on. For example, jQuery Validate's shim might look like the following:

shim: {
    // ...
    "jquery-validate": {
        deps: [ "jquery" ]
    }
}

Single- vs Multi-Page Apps

It's common to include all the necessary JavaScript in a single page application. So, you may define the configuration and the initial require of a single-page application in main.js like so:

require.config({
    paths: {
        ko: "vendor/knockout-min",
        postal: "vendor/postal",
        underscore: "vendor/underscore-min",
        amplify: "vendor/amplify"
    },
    shim: {
        ko: {
            exports: "ko"
        },
        underscore: {
            exports: "_"
        },
        amplify: {
            exports: "amplify"
        }
    },
    baseUrl: "/js"
});

require( [ "https://twitter.com/jrburkeapp" ], function( App ) {
    App.init();
})

You might also need separate pages that not only have page-specific modules, but share a common set of modules. James Burke has two repositories that implement this type of behavior.

The rest of this article assumes you're building a multi-page application. I'll rename main.js to common.js and include the necessary require.config in the above example in the file. This is purely for semantics.

Now I'll require common.js in my files, like this:

<script src="js/require-jquery.js"></script>
    <script>
        require( [ "./js/common" ], function () {
            //js/common sets the baseUrl to be js/ so
            //can just ask for 'app/main1' here instead
            //of 'js/app/main1'
            require( [ "pages/index" ] );
        });
    </script>
</body>
</html>

The require.config function will execute, requiring the main file for the specific page. The pages/index main file might look like the following:

require( [ "app", "postal", "ko", "viewModels/indexViewModel" ], function( app, postal, ko, IndexViewModel ) {
    window.app = app;
    window.postal = postal;

    ko.applyBindings( new IndexViewModel() );
});

This page/index module is now responsible for loading all the neccessary code for the index.html page. You can add other main files to the pages directory that are also responsible for loading their dependent modules. This allows you to break multi-page apps into smaller pieces, while avoiding unnecessary script inclusions (e.g. including the JavaScript for index.html in the about.html page).


Sample Application

Let's write a sample application using this approach. It'll display a searchable list of beer brands and let us choose your favorites by clicking on their names. Here is the app's folder structure:

"Folder structure"

Let's first look at index.html's HTML markup:

<section id="main">
    <section id="container">
        <form class="search" data-bind="submit: doSearch">
            <input type="text" name="search" placeholder="Search" data-bind="value: search, valueUpdate: 'afterkeydown'" />
            <ul data-bind="foreach: beerListFiltered">
                <li data-bind="text: name, click: $parent.addToFavorites"></li>
            </ul>
        </form>

        <aside id="favorites">
            <h3>Favorites</h3>
            <ul data-bind="foreach: favorites">
                <li data-bind="text: name, click: $parent.removeFromFavorites"></li>
            </ul>
        </aside>
    </section>
</section>

<!-- import("templates/list.html") -->

<script src="js/require-jquery.js"></script>
<script>
    require( [ "./js/common" ], function (common) {
        //js/common sets the baseUrl to be js/ so
        //can just ask for 'app/main1' here instead
        //of 'js/app/main1'
        require( [ "pages/index" ] );
    });
</script>

Pages

The structure of our application uses multiple "pages" or "mains" in a pages directory. These separate pages are responsible for initializing each page in the application.

The ViewModels are responsible for setting up the Knockout bindings.

ViewModels

The ViewModels folder is where the main Knockout.js application logic lives. For example, the IndexViewModel looks like the following:

// https://github.com/jcreamer898/NetTutsKnockout/blob/master/lib/js/viewModels/indexViewModel.js
define( [
    "ko",
    "underscore",
    "postal",
    "models/beer",
    "models/baseViewModel",
    "shared/bus" ], function ( ko, _, postal, Beer, BaseViewModel, bus ) {

    var IndexViewModel = function() {
        this.beers = [];
        this.search = "";

        BaseViewModel.apply( this, arguments );
    };

    _.extend(IndexViewModel.prototype, BaseViewModel.prototype, {
        initialize: function() { // ... },

        filterBeers: function() { /* ... */ },

        parse: function( beers ) { /* ... */ },

        setupSubscriptions: function() { /* ... */ },

        addToFavorites: function() { /* ... */ },

        removeFromFavorites: function() { /* ... */ }
    });

    return IndexViewModel;
});

The IndexViewModel defines a few basic dependencies at the top of the file, and it inherits BaseViewModel to initialize its members as knockout.js observable objects (we'll discuss that shortly).

Next, rather than defining all of the various ViewModel functions as instance members, underscore.js's extend() function extends the prototype of the IndexViewModel data type.

Inheritance and a BaseModel

Inheritance is a form of code reuse, allowing you to reuse functionality between similar types of objects instead of rewriting that functionality. So, it's useful to define a base model that other models or can inherit from. In our case, our base model is BaseViewModel:

var BaseViewModel = function( options ) {
    this._setup( options );

    this.initialize.call( this, options );
};

_.extend( BaseViewModel.prototype, {
    initialize: function() {},

    _setup: function( options ) {
        var prop;

        options = options || {};

        for( prop in this ) {
            if ( this.hasOwnProperty( prop ) ) {
                if ( options[ prop ] ) {
                    this[ prop ] = _.isArray( options[ prop ] ) ?
                        ko.observableArray( options[ prop ] ) :
                        ko.observable( options[ prop ] );
                }
                else {
                    this[ prop ] = _.isArray( this[ prop ] ) ?
                        ko.observableArray( this[ prop ] ) :
                        ko.observable( this[ prop ] );
                }
            }
        }
    }
});

return BaseViewModel;

The BaseViewModel type defines two methods on its prototype. The first is initialize(), which should be overridden in the subtypes. The second is _setup(), which sets up the object for data binding.

The _setup method loops over the properties of the object. If the property is an array, it sets the property as an observableArray. Anything other than an array is made observable. It also checks for any of the properties' initial values, using them as default values if necessary. This is one small abstraction that eliminates having to constantly repeat the observable and observableArray functions.

The "this" Problem

People who use Knockout tend to prefer instance members over prototype members because of the issues with maintaining the proper value of this. The this keyword is a complicated feature of JavaScript, but it's not so bad once fully grokked.

From the MDN:

"In general, the object bound to this in the current scope is determined by how the current function was called, it can't be set by assignment during execution, and it can be different each time the function is called."

So, the scope changes depending on HOW a function is called. This is clearly evidenced in jQuery:

var $el = $( "#mySuperButton" );
$el.on( "click", function() {
    // in here, this refers to the button
});

This code sets up a simple click event handler on an element. The callback is an anonymous function, and it doesn't do anything until someone clicks on the element. When that happens, the scope of this inside of the function refers to the actual DOM element. Keeping that in mind, consider the following example:

var someCallbacks = {
    someVariable: "yay I was clicked",
    mySuperButtonClicked: function() {
        console.log( this.someVariable );
    }
};

var $el = $( "#mySuperButton" );
$el.on( "click", someCallbacks.mySuperButtonClicked );

There's an issue here. The this.someVariable used inside mySuperButtonClicked() returns undefined because this in the callback refers to the DOM element rather than the someCallbacks object.

There are two ways to avoid this problem. The first uses an anonymous function as the event handler, which in turn calls someCallbacks.mySuperButtonClicked():

$el.on( "click", function() {
    someCallbacks.mySuperButtonClicked.apply();
});

The second solution uses either the Function.bind() or _.bind() methods (Function.bind() is not available in older browsers). For example:

$el.on( "click", _.bind( someCallbacks.mySuperButtonClicked, someCallbacks ) );

Either solution you choose will achieve the same end-result: mySuperButtonClicked() executes within the context of someCallbacks.

"this" in Bindings and Unit Tests

In terms of Knockout, the this problem can show itself when working with bindings--particularly when dealing with $root and $parent. Ryan Niemeyer wrote a delegated events plugin that mostly eliminates this issue. It gives you several options for specifying functions, but you can use the data-click attribute, and the plugin walks up your scope chain and calls the function with the correct this.

<form class="search">
    <input type="text" name="search" placeholder="Search" data-bind="value: search" />
    <ul data-bind="foreach: beerListFiltered">
        <li data-bind="text: name, click: $parent.addToFavorites"></li>
    </ul>
</form>

In this example, $parent.addToFavorites binds to the view model via a click binding. Since the <li /> element resides inside a foreach binding, the this inside $parent.addToFavorites refers to an instance of a the beer that was clicked on.

To get around this, the _.bindAll method ensures that this maintains its value. Therefore, adding the following to the initialize() method fixes the problem:

_.extend(IndexViewModel.prototype, BaseViewModel.prototype, {
    initialize: function() { 
        this.setupSubscriptions();

        this.beerListFiltered = ko.computed( this.filterBeers, this );

        _.bindAll( this, "addToFavorites" );
    },
});

The _.bindAll() method essentially creates an instance member called addToFavorites() on the IndexViewModel object. This new member contains the prototype version of addToFavorites() that is bound to the IndexViewModel object.

The this problem is why some functions, such as ko.computed(), accepts an optional second argument. See line five for an example. The this passed as the second argument ensures that this correctly refers to the current IndexViewModel object inside of filterBeers.

How would we test this code? Let's first look at the addToFavorites() function:

addToFavorites: function( beer ) {
    if( !_.any( this.favorites(), function( b ) { return b.id() === beer.id(); } ) ) {
        this.favorites.push( beer );
    }
}

If we use the mocha testing framework and expect.js for assertions, our unit test would look like the following:

it( "should add new beers to favorites", function() {
    expect( this.viewModel.favorites().length ).to.be( 0 );

    this.viewModel.addToFavorites( new Beer({
        name: "abita amber",
        id: 3
    }));

    // can't add beer with a duplicate id
    this.viewModel.addToFavorites( new Beer({
        name: "abita amber",
        id: 3
    }));

    expect( this.viewModel.favorites().length ).to.be( 1 );
});

To see the full unit testing setup, check out the repository.

Let's now test filterBeers(). First, let's look at its code:

filterBeers: function() {
    var filter = this.search().toLowerCase();

    if ( !filter ) {
        return this.beers();
    }
    else {
        return ko.utils.arrayFilter( this.beers(), function( item ) {
            return ~item.name().toLowerCase().indexOf( filter );
        });
    }
},

This function uses the search() method, which is databound to the value of a text <input /> element in the DOM. Then it uses the ko.utils.arrayFilter utility to search through and find matches from the list of beers. The beerListFiltered is bound to the <ul /> element in the markup, so the list of beers can be filtered by simply typing in the text box.

The filterBeers function, being such a small unit of code, can be properly unit tested:

 beforeEach(function() {
    this.viewModel = new IndexViewModel();

    this.viewModel.beers.push(new Beer({
        name: "budweiser",
        id: 1
    }));
    this.viewModel.beers.push(new Beer({
        name: "amberbock",
        id: 2
    }));
});

it( "should filter a list of beers", function() {
    expect( _.isFunction( this.viewModel.beerListFiltered ) ).to.be.ok();

    this.viewModel.search( "bud" );

    expect( this.viewModel.filterBeers().length ).to.be( 1 );

    this.viewModel.search( "" );

    expect( this.viewModel.filterBeers().length ).to.be( 2 );
});

First, this test makes sure that the beerListFiltered is in fact a function. Then a query is made by passing the value of "bud" to this.viewModel.search(). This should cause the list of beers to change, filtering out every beer that does not match "bud". Then, search is set to an empty string to ensure that beerListFiltered returns the full list.


Conclusion

Knockout.js offers many great features. When building large applications, it helps to adopt many of the principles discussed in this article to help your app's code remain manageable, testable, and maintainable. Check out the full sample application, which includes a few extra topics such as messaging. It uses postal.js as a message bus to carry messages throughout the application. Using messaging in a JavaScript application can help decouple parts of the application by removing hard references to each other. Be sure and take a look!

Advertisement