Backbone vs. Knockout


If you are asking yourself, which Javascript framework should I use for my next application, then this is the article for you.

When building for the Web, there is no single standard for how you should structure your application. There are best practices and frameworks which arise from preferred workflows, but ultimately you’re free to use the tools and follow the workflow that work best for you.

One typical question before starting is what JavaScript framework you should use, or if you should use one at all.

This article is devoted to one tiny aspect of this process. It compares how some recurring functionality can be implemented in Backbone.js/Marionette and Knockout.js.

If Backbone.js and Knockout.js are candidates for your next project, maybe this detailed article will help you choose.

backboneknockout

The reason why we chose these two frameworks for this article is simple, for our application Ghostlab, we’ve implemented the user interface as a cross-platform HTML5 app using Knockout.js. We’ve been happy with that choice since. At the same time, our team has plenty of experience with Backbone.js as well. So, for a little prototype, and to compare, we’ve decided to use both of them in separate implementations.

Scroll to the bottom of this post to download the BONUS RESOURCE (the Backbone.js and Knockout.js application source files).

An overview of Backbone and Knockout

Backbone

Backbone provides you with:

  1. View, Backbone.
  2. Model and Backbone.
  3. Collection components.

Those three basic classes come with built-in functionalities like:

  • rendering
  • templating
  • event handling
  • data fetching
  • model
  • collection events

Lets take a look at a basic Backbone.js example below:

<script id="myTemplate" type="text/template">
    <input type="text" id="textInput">
     Hello <span id="name"><%=foo.get('name') %></span>!
</script>

<div id="myView"></div>
        	
var myModel = new Backbone.Model({name: 'Jane'});

var MyView = Backbone.View.extend({
            el: '#myView',
            model: myModel,
            events: {
                'input #textInput': 'inputUpdate'
            },
            inputUpdate: function () {
                // update the model when the value of the input element has changed
                myModel.set('name', $  ('#textInput').val());
            },
            initialize: function () {
                // update the input element when the model value has changed
                myModel.on('change:name', function () {
                    $  ('#name').html($  ('#textInput').val());
                });
                this.render();
            },
            render: function () {
                var template = _.template($  ('#myTemplate').html(), {foo: this.model});
                this.$  el.html(template);
            }
});

    	new MyView();

It’s possible to use Backbone models and collections as they are provided. If your application requires custom functionality, you can implement it by extending backbone core – i.e. by creating your own, default-extending view.

As an argument to the extend function, pass your overrides for built-in functions as well as private members.

The initialize function is executed when the view is instantiated. We are directly passing a model onto the view – we could do the same thing with a collection.

Inside the render function, we are using underscore to render the data with our script template.

Underscore templating comes with Backbone.js by default, but Backbone.js also works with other template engines like Handlebars.

Our plugin of choice for this article, however, is Backbone.Marionette. We will see it in action in the next sections of this article.

Knockout

The magic of Knockout are two-way data bindings: whenever you update a UI field the underlying model variable updates its contents, and vice versa. Consider this simple example to get an idea how it feels like writing Knockout code:

<input type="text" data-bind="value: myName">
<div>Hello <span data-bind=”text: myName”></span>!</div>

<script>
    // define the model
    var model = {
        myName: ko.observable('Jane’')
    };

    // start Knockout
	    ko.applyBindings(model);
</script>

In the script, the model is defined, Knockout is started with that model (by calling ko.applyBindings). The model contains a property myName, a ko.observable, which is what makes the two-way binding possible (if you only need a one-way binding, you can also use a regular string instead of the ko.observable).

ko.observable creates a function, so the property value has to be accessed like so:

	var myNameValue = model.myName();
	model.myName(‘Tarzan’);

In the UI code, you can see that the input and the span elements have a data-bind attribute, which binds myName to the elements via the text binding. The observable listens for changes on the value and updates the UI accordingly.

Conversely, the data-bind on the <input> element attaches an event handler which updates the model variable. Thus, when you type your name into the text box, the contents of the <span> will update once the change event has fired.

Of course, Knockout comes with more bindings than just text and value bindings. There are bindings for events (e.g., click), to set content, attributes, and for control flow (e.g., to iterate over a collection). We’ll discuss some examples below.

Case Study Comparison – Example Application in Backbone & Knockout

In the following, we’ll discuss a minimal example application which highlights how certain common features can be implemented both in Backbone and Knockout.

The example app mocks the client side of a file sharing app, which:

  • displays a list of files residing on some (mocked) server,
  • lets you add files to the list by dragging and dropping them in your browser,
  • lets you delete files from the list,
  • lets you search (filter) and sort the list,
  • shows a separate list of files, which are currently being uploaded, and their upload progress.

Basic Application Structure

Backbone

A basic Backbone application typically consists of three elements – a view, a model and a collection. And this is what we’ll be looking at in this example. To make it a little more interesting, we’ll use an extension of Backbone.Marionette.CompositeView (GridView) instead of just a Backbone.View. The models and collections will be used as provided by Backbone.Model and Backbone.Collection.

var fileCollection = new Backbone.Collection();
            var gridView = new GridView({
                collection: fileCollection
            });

            gridView.render();
            $  ('#grid').html(gridView.el);

Above, we create an instance of Backbone.Collection. Since the GridView is instantiated with the fileCollection, it will automatically update the view whenever the model/collection is changed. In the final line above, we append the GridView’s element to the DOM.

Knockout.js

In Knockout you typically have one main view model, which can include sub-models.

For our application, we call our main view model “App” and instantiate and start it as soon as the DOM has been built. In the sample code, we include this script in our HTML:

<script>
    // create and start the app
    var app = new App();
    app.start();
    ko.applyBindings(app);
</script>

Our App class instantiates the sub-models, a collection of files and the queue of uploaded files:

	function App() {
	    this.files = new FileCollection();
	    this.queues = ko.observableArray();
	}

	App.prototype.start = function() {
	    // load data
	};

	function FileCollection() {
	    // items will be instances of FileModel
	    this.items = ko.observableArray();
	}

	function FileModel(name) {
	    this.name = ko.observable(name || '');
	}

The code demonstrates that we can use our own FileCollection class (into which we can package some custom functionality), or just use a ko.observableArray coming with Knockout. The latter has a similar interface to the standard JavaScript array (e.g., it supports push and splice methods), but listens to changes on the array and updates the UI accordingly.

Displaying the file list

Backbone

As mentioned above, the rendering of the collection is handled by our CompositeView, the GridView. In the code below, we first define the two templates to display the data. Then, we define a composite and an item view using these templates.

<script id="row-template" type="text/template">
    <td><%= name %></td>
    <td><a href="javascript:void(0);" class="js_remove_file">Remove</a></td>
</script>

<script id="grid-template" type="text/template">
    <thead>
        <tr><th class="headName">Name</th></tr>
    </thead>
    <tbody></tbody>
</script>

var GridRow =  Backbone.Marionette.ItemView.extend({
            template: '#row-template'
});

var GridView = Backbone.Marionette.CompositeView.extend({
            template: "#grid-template",
            itemView: GridRow,
            appendHtml: function(collectionView, itemView) {
                collectionView.$  ('tbody').append(itemView.el);
            }
});

Knockout.js

Once the models have been set up and populated, no special JavaScript code is needed to display the collection items.

In our HTML, the foreach binding is used on the container element. This binding replicates the DOM structure contained in the container element for each element of the collection passed as an argument to foreach. It is worth noting that within a foreach binding the context is switched to the collection element of the current iteration, so name in the text binding of the <td> element refers to the current FileModel instance.

<table>
    <tbody data-bind="foreach: files.items">
        <tr>
            <td data-bind="text: name"></td>
        </tr>
    </tbody>
</table>

Retrieving data from a REST service

Assume we have a REST service listing the files currently saved on the server. The service lists the files in the following JSON structure:

[
	    { "name": "tarzan-of-the-apes.pdf" },
	    …
]

Backbone

Backbone models and collections come with a built-in implementation of REST services.

var FileCollection = Backbone.Collection.extend({
    url: 'files.json'
});
var fileCollection = new FileCollection();
fileCollection.fetch();

Calling fetch() on the fileCollection will result in a GET request to files.json and populate the collection. Each entry will be an instance of Backbone.Model. You can pass success and error callbacks to fetch via the options hash. Also, you can subscribe to the sync event on the collection, which will be triggered when the collection has been successfully synchronized with the server.

fileCollection.on('sync', function (collection) {
	console.log(‘Collection updated from server’, collection);
});

Knockout.js

Knockout doesn’t come with its own AJAX facilities, so this has to be done using some other library such as jQuery or Zepto.js (or of course using plain JavaScript if you’re inclined to do so).

The Knockout core also doesn’t provide a means to populate the model with data loaded via AJAX. There is a plugin for that, but most of the time, we just do it manually.

If in your model not every variable is an observable, this might result in some amount of code if you want to do it in a generic way, but for simple data like in the case of our example application it’s not so hard.

The following code uses jQuery’s ajax function and invokes the load method on the FileCollection, which creates new array entries.

	App.prototype.start = function() {
	    var self = this;
	    $  .ajax({
	        url: 'files.json'
	    }).done(function(data) {
	        self.files.load(data);
	    });
	};

	FileCollection.prototype.load = function(data) {
	    // remove old items (using the “splice” method to keep the current observableArray)
	    this.items.splice(0, this.items().length);
	    // add new items
	    for (var i = 0, len = data.length; i < len; i++) {
	        this.items.push(new FileModel(data[i].name));
	    }
	};

Deleting items

Backbone

Backbone.Model provides a destroy method. Calling it will automatically trigger a DELETE request to the REST API, which will remove the item in question. There are two conditions for the request to be triggered: the urlRoot property of the model has to be defined, and the item has to have an id attribute.

Similar to sync and fetch, you can pass an option hash to the destroy method, providing custom callbacks or, as in our example below, a relative URL for the DELETE request.

               this.model.destroy({
                    url: '/api/files',
                    emulateJSON: true,
                    data: { path: this.model.get('name') },
                    complete: function () {
//trigger events here, the DELETE request is complete
                    }
                });

Of course, the deletion of items will be triggered by a user action. Below, you can see how we set up our GridView to handle click events on a DOM element. Note that we do not have to take care of identifying the item to be deleted, Marionette handles this for us.

<a href="javascript:void(0);" class="js_remove_file">Remove</a>

var GridRow = Backbone.Marionette.ItemView.extend({
            events: {
                'click .js_remove_file': 'trash'
            },
             trash: function () {
                this.model.destroy({ /* see above… */ });
            }
});

Knockout.js

The issue with deleting items from an array is that you'll need to know the index of the item you want to delete. Knockout has a built-in variable (actually, an observable) for this, which is meaningful when in a foreach binding: $ index.

Imagine this delete link being next to item in a table cell within the files table:

	<a data-bind="click: app.files.remove($  index())">Delete file</a>

This binds a click handler to the remove method of the FileCollection, which deletes an item at a specific index (specifically, it returns a function which does that, because the handler above actually does a function call, so we need this function to return the event handler):

	FileCollection.prototype.remove = function(index) {
	    var self = this;
	    return function() {
	        self.items()[index].remove();
	        self.items.splice(index, 1);
	    };
	};

The event handler calls the remove method on the FileModel and then actually deletes the item from the array. We added a remove method on the FileModel so we can do some extra per-item cleanup work, such as notifying the REST service that an item is about to be deleted (as Knockout doesn't know about any REST services, so this has to be done manually):

	FileModel.prototype.remove = function() {
	    // notify the server of the file deletion; something like
	    $  .ajax({
	        url: 'files.json',
	        type: 'DELETE',
	        data: { path: this.name() }
	    })
	};

Search

Backbone

In our sample application, a search updates the view in real time. This means that whenever the search term changes, we go through the entire (unfiltered) collection and look for matches. Once we have gone through the entire collection, we update the collection to only display the items that match the search term. For this purpose, we use the reset method on the collection.

However, we don't want to loose the original collection - we'll need it once the search filter is reset or changed. To accomplish this, we need to cache the original collection. The cache is set initially and only updated in case the collection is updated from the server, not in case it is updated via the search.

var FileCollection = Backbone.Collection.extend({
            updated: function (models, options) {
    // collection can be reset for several reasons
    // below condition is just making sure that it was reset after successful fetch
    // of models. So this.cache will always keep the full copy of files list
                if (options.success) {
                    this.cache = models.clone();
                }
            },

            initialize: function () {
                this.on('reset', this.updated);
            },

           search: function (query) {
                var matches = [],
                    pattern = new RegExp(jQuery.trim(query).replace(/ /gi, '|'), 'i');
                this.cache.each(function(model) {
                        if (pattern.test(model.get('name'))) {
                            matches.push(model);
                    }
                });

                this.reset(matches);
            }
});

var fileCollection = new FileCollection();
fileCollection.fetch();

The code below shows how we can attach our search functionality to the interface by extending the Marionette ItemView.

var headerView = Backbone.Marionette.ItemView.extend({
            template: "#header-template",
            events: {
                'keyup .searchTerm': 'search',
                'click .searchButton': 'search'
            },
            ui: {
                'search': 'input[class=searchTerm]'
            },
            search: function () {
                 fileCollection.search(this.ui.search.val());
            }
  });

The code below shows how we can attach our search functionality to the interface by extending the Marionette ItemView.

var headerView = Backbone.Marionette.ItemView.extend({
            template: "#header-template",
            events: {
                'keyup .searchTerm': 'search',
                'click .searchButton': 'search'
            },
            ui: {
                'search': 'input[class=searchTerm]'
            },
            search: function () {
                 fileCollection.search(this.ui.search.val());
            }
  });

Knockout.js

First, we set up the HTML. We need a text field in which the user can enter the search string:

<input class=”searchTerm” type="text" placeholder="What are you looking for?" data-bind="
		value: files.searchTerm,
		valueUpdate: 'keyup'">

We've used the value binding to bind the input element's value to a new observable searchTerm added to our FileCollection (see below), and we tell Knockout to update the model value whenever a key is released by setting the valueUpdate binding to the string keyup. (By default, the value is update on the change event, i.e., typically when the input field loses focus.)

Search actually means filtering. In Knockout, filtering a collection can be implemented with a really cool feature, "computed observables". So we'll create a computed observable within the FileCollection, displayedItems. In the HTML, we use this new computed observable for the foreach binding instead of using the items directly:

<table>
    <tbody data-bind="foreach: files.displayedItems">
        <tr>
            <td data-bind="text: name"></td>
            <td><a data-bind="click: app.files.remove($  index)">Delete file</a></td>
        </tr>
    </tbody>
</table>

Next, we implement the computed observable. It is created by passing a function returning the computed value to ko.computed:

	function FileCollection() {
	    var self = this;

	    // the base collection
	    this.items = ko.observableArray();

	    // searching and sorting state
	    this.searchTerm = ko.observable('');

	    // computed observable returning the items to display
	    // in order to support filtering (the search function) and sorting
	    this.displayedItems = ko.computed(function() {
	        var matches = [],
	            pattern = new RegExp(self.searchTerm().replace(/ /gi, '|'), 'i'),
	            item;

	        // filter
	        for (var i = 0, len = self.items().length; i < len; i++) {
	            item = self.items()[i];
	            if (pattern.test(item.name())) {
	                matches.push(item);
	            }
	        }

	        return matches;
	    });
	}

In the function passed to ko.computed, we create an array, all the items that match the search string, and return it.

Sorting

Backbone

If you require custom sorting for your collection, you can simply provide a comparator function.

var FileCollection = Backbone.Collection.extend({
            sortDirection: -1,
            comparator: function(item, item2) {
   	    this.sortDirection = -this.sortDirection;
                return item.get('name').localeCompare(item2.get('name')) * this.sortDirection;
            }
});

Again, we need to attach our sorting functionality to the interface.

var GridView = Backbone.Marionette.CompositeView.extend({
        events: {
             'click th.headName': 'sortByName',
        },
        sortByName: function () {
                fileCollection.sort();
        }
});

Knockout.js

Sorting is really an extension to searching. Basically, after creating the array with the items matching the search string, we simply sort this array according to some conditions.

In the HTML, we add a header row to sort the list:

<table>
    <thead>
        <tr><th data-bind="click: app.files.createSortingSetter('name')">Name</th></tr>
    </thead>
    …
</table>

In the FileCollection constructor, we add some more fields related to sorting (the name of the field we want to sort by, the sort direction, which can be either 1 (sort ascending) or -1 (sort descending), and we memorize the name of the field we used for sorting previously, so we can switch sorting directions by clicking a sort header field once more:

function FileCollection() {
    …
    // searching and sorting state
    this.searchTerm = ko.observable('');
    this.sortField = ko.observable('name');
    this.sortDirection = ko.observable(1);
    this.lastSortField = this.sortField();
   ...
}

The new method createSortingSetter really just sets the sorting field and the direction.

Again, this method returns an event handler, i.e., a function.

FileCollection.prototype.createSortingSetter = function(sortField) {
    var self = this;

    return function() {
        self.sortField(sortField);
        if (self.lastSortField === sortField) {
            self.sortDirection(-self.sortDirection());
        }
        self.lastSortField = sortField;
    };
}

Finally, we add the actual sorting code to the displayedItems computed observable:

Finally, we add the actual sorting code to the “displayedItems” computed observable:

    this.displayedItems = ko.computed(function() {
        var matches = [],
            item;

        // filter (as above); fills the “matches” array

        // sort
        matches.sort(function(o1, o2) {
            var sortField = self.sortField(),
                sortDirection = self.sortDirection(),
                value1 = o1[sortField],
                value2 = o2[sortField];
            return value1().localeCompare(value2()) * sortDirection;
        });

        return matches;
    });

Dropping Files

Backbone.js

In order to support dropping files, we are using a top-level view that covers the entire viewport and handles drag and drop events. Preventing default on the events is required for the browser not to navigate to the files dropped onto the browser. Again, setting up the behavior is easy thanks to extending the Marionette ItemView.

Backbone.Marionette.ItemView.extend( {
            tagName: 'div',
            template: template,
            events: {
                'dragenter' : 'dropHighlight',
                'dragleave' : 'dropUnhighlight',
                'drop' : 'drop',
                'dragover': 'dropOver'
            },

            drop: function (event) {
           	    event.preventDefault();
                var files = event.originalEvent.dataTransfer.files,
		name;
	    for (var i = 0, len = files.length; i < len; i++) {
	name = files[i].name;
	uploadMock({ name: name, percent: 0});	
	    }
            },

            dropHighlight: function (e) {
                e.preventDefault();
            },

            dropUnhighlight: function (e) {
                e.preventDefault();
            },

            dropOver: function (e) {
                e.preventDefault();
            }
 });

Knockout.js

Knockout comes with a varied, extensive set of bindings, but it doesn't provide an own binding for every event. If there is no specific binding exists, the generic event binding can be used instead:

<div data-bind="
    event: {
        drop: app.dropFile,
        dragover: function(data, event) { event.preventDefault(); },
        dragleave: function(data, event) { event.preventDefault(); },
        dragend: function(data, event) { event.preventDefault(); }
    }">
	</div>

In this code, event handlers for drag&drop events are registered. Most of them just prevent the default actions by calling event.preventDefault(). Note that it is perfectly legal to use anonymous functions as arguments to bindings (although generally, you might want to keep the logic separate from the HTML).

The drop event is bound to this event handler:

App.prototype.dropFile = function($  data, event) {
    var files = event.originalEvent.dataTransfer.files,
        name;

    for (var i = 0, len = files.length; i < len; i++) {
        name = files[i].name;
        this.files.add(new FileModel(name));
        this.queues.push(new QueueItemModel(name));
    }
}

It creates a new FileModel and adds it to the files collection. It also creates a new QueueItemModel (see below) and adds it to the queues collection.

Updating Progress Bars

Backbone.js

Files that are being uploaded are added to a dedicated ProgressCollection. This collection is used by the progress bar view, a CompositeView with ItemViews representing individual file uploads.

            var progressList = new ProgressCollection();
            var progressView = new ProgressView({
                collection: progressList
            });

Since this is a mock application, we also mock the upload. Below, you can see how we use timeouts to simulate progress on a file upload, and add the file to the file collection once it has been uploaded completely.

function uploadMock(queue) {
        // Progress is a Backbone.Model
        var item = new Progress({
                name: queue.name,
                percent: 0
        });

        // mock an upload function
        function upload() {
          var percent = Math.min(item.get('percent') + Math.random() * 10, 100);
          item.set('percent', percent);
          if (percent < 100) {
            setTimeout(upload, 100);
          } else {
            fileCollection.add(item);
            fileCollection.reset(fileCollection.models, {success: true});
          }
        }

        setTimeout(upload, 100);
        progressList.add(item);
}

Our ProgressRow view (an ItemView) draws the current progress via the CSS width property. Whenever the underlying model changes (when we set a new percentage value on the item), we call the renderProgress function to update the style.

<script id="progress-row-template" type="text/template">
        <td><%= name %></td>
        <td><span class="progress" style="width: <%= percent %>%;"></span></td>
    </script>

        var ProgressRow = Backbone.Marionette.ItemView.extend({
            template: "#progress-row-template",
            tagName: "tr",

            'modelEvents': {
                'change': 'renderProgress'
            },

            ui: {
              progress: ‘.progress',
            },

            renderProgress: function () {
                // render each row only once
                if (this.model.get('percent') === 0) {
                    this.render();
                } else {
                    this.ui.progress.css('width', this.model.get('percent')+'%');
                }
            }
        });

Knockout.js

This final snippet shows how dynamic changes to model variables update the UI. Our new QueueItemModel has a percent property, an observable, which indicates how many percent of the file has been uploaded to the server (we just mock the uploading here):

function QueueItemModel(name) {
	    var self = this;
	    this.name = name;
	    this.percent = ko.observable(0);

	    // mock an upload function
	    function upload() {
	        var percent = Math.min(self.percent() + Math.random() * 10, 100);
	        self.percent(percent);
	        if (percent < 100) {
	            setTimeout(upload, 100);
	        }
	    }
	    setTimeout(upload, 100);
}

In the HTML, we create a table showing all the files in the upload queue and the upload progresses:

<table data-bind="foreach: queues">
    <tr>
        <td data-bind="text: name"></td>
        <td>
            <span data-bind="style: { width: percent() + '%' }"></span>
        </td>
    </tr>
</table>

The interesting part here is in the style binding of the second <span>. It implements a home-grown progress bar by setting the width property of the element's inline style to the percent model value. Note that because of the string concatenation percent() + ‘%' we actually have to use parentheses on percent to extract the value, otherwise you'd get the actual observable (i.e., the function). Internally, not using a single observable as value actually implicitly creates a new computed observable and assigns that to the binding.

Backbone vs Knockout Comparison Conclusion

Backbone.js

Backbone comes with a lot of built-in functionality that speeds up your development, such as REST support, underscore templating, and jQuery-like event binding. By prescribing a specific architecture for your application, Backbone also helps teams of developers maintain a clean code base.

It should be noted though that the learning curve for Backbone is quite steep. If you only want to build a small application, following its architecture can make you less flexible.

Knockout.js

Knockout is simple to set up and allows you to develop your application quickly - the two-way data binding is a powerful mechanism to sync your model with the UI. The strict separation of the view (HTML) and the business logic (JavaScript) makes your code very clean - for example, you will not have any DOM selectors in your JavaScript app.

In essence, Knockout links the script and markup layers in your application, thus granting you complete freedom over the structure of your JavaScript application.

However, filling the model with your data can be cumbersome, since Knockout does not provide a built-in functionality to convert plain objects to nested observables (there is a plugin for this, though).

And if you are looking for a JavaScript framework for structuring your application, Knockout may not be the best choice because it really focuses on data binding.

Bonus Resource

If you have enjoyed this in-depth post why not download the source files, both example applications are included.

It should be pointed out that the worlds of Backbone and Knockout.js aren't mutually exclusive. There are projects like Knockback.js that combine the two and "bring Knockout.js magic to Backbone.js."


The post Backbone vs. Knockout appeared first on Speckyboy Web Design Magazine.


Speckyboy Web Design Magazine

Leave a Comment