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:
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.
"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!
Comments