Knockout.js vs. Backbone.js – Part 1

So I’ve recently been diving into web client frameworks and decided to compare Knockout.js and Backbone.js.  To compare the two, I chose a very simple scenario (based on a user request we recently had to implement in an ASP.NET web forms site) – the user wanted the ability to query for the status of a SQL Agent job.  I already had a service that returned metadata about a SQL Agent Job, so I simply needed to create a way for users to select a job (or more), and view the status on the screen.

The Gist

At the outset, I wanted to keep it simple, so I set the following requirements:

  • Our existing legacy “.asmx” web service was going to be the data source.
  • The UI needed to be able to display any number of SQL Agent jobs at a time.
  • The data for each SQL Agent job should continuously refresh at short intervals (2 seconds or so)

Backbone

What exactly is Backbone.js?  Backbone.js is an MVC framework for the web client (i.e.- the browser).  In their own words:

“Backbone supplies structure to JavaScript-heavy applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON interface.”

It’s important to bear in mind that in my exploration of Backbone.js, I’ve barely scratched the surface.  In fact, my simple example doesn’t actually extend Backbone’s controller type.  As a result, I didn’t take advantage of the interesting ability to use URL fragments to map to controller actions and events.  Instead, what I was looking for was a brief and quick introduction to the framework.  (Note: one of the example Backbone.js apps also omits the use of controllers, while still demonstrating a large set of interesting features – arguably much more interesting than my example!.)

The Service

Our legacy web service takes the string name of a SQL Agent job as an argument and returns an object with the following members:

  • Job Name
  • Current Step
  • Last Start Date
  • Last End Date
  • Last Run Time
  • Next Run Date
  • Last Outcome Message

A typical response will look like this:

image

The Models

We have two models – a “SqlJobStatus” model that represents the data listed above, and a “SqlStatusJobCollection” model that represents a container of “SqlJobStatus” models. 

$(function(){
    window.SqlJobStatus = Backbone.Model.extend({

        defaults: {
                    JobName: "",
                    CurrentStep: "",
                    LastStartDate: "",
                    LastEndDate: "",
                    LastRunTime: "",
                    LastOutcomeMessage: "",
                    NextRunDate: "",
                    CssClass: "idle"
        },

        initialize: function() {
            _.bindAll(this, "fetch", "clear", "updateData");
        },

        fetch: function(options) {
            if(this.get('JobName') != "")
            {
                $.ajax({
                            type        : "POST",
                            url         : "http://localhost/SqlAgentHelper.asmx/GetJobStatus",
                            data        : '{"jobName":"' + this.get('JobName') + '" }',
                            contentType : "application/json",
                            dataType    : "json",
                            success     : this.updateData,
                            error       : function() { alert('There was an error retrieving the status of the job.'); }
                });
            }
        },

        clear: function() {
            this.view.remove();
        },

        updateData: function(result) {
            this.set({
                                JobName : result.d.JobName,
                                CurrentStep : result.d.CurrentStepName == undefined ? "N/A" : result.d.CurrentStepName,
                                LastEndDate : result.d.LastEndDate == undefined ? "N/A" : result.d.LastEndDate.to_date(),
                                LastOutcomeMessage : result.d.LastOutcomeMessage,
                                LastRunTime : result.d.LastRunTime == undefined ? "N/A" : result.d.LastRunTime,
                                LastStartDate : result.d.LastStartDate == undefined ? "N/A" : result.d.LastStartDate.to_date(),
                                NextRunDate : result.d.NextRunDate == null ? "N/A" : result.d.NextRunDate,
                                CssClass : (result.d.CurrentStepName == undefined || result.d.CurrentStepName == "") ? "idle" : "active"
                            },{silent:true});
            this.change();
         }
    });

    window.SqlJobStatusCollection = Backbone.Collection.extend({
        model: SqlJobStatus

    });
});


Let’s this break down:

  • “defaults” – initializing members with default values. (this is a Backbone.js standard member)
  • “initialize” – called when the model is created.  In this case, we’re simply binding the context of “this” in the scope of the function names provided to be the model object. (this is a Backbone.js standard member)
  • “fetch” – I’ve overridden the fetch function to use a standard ajax call.  Backbone.js prefers to work with RESTful services, not the legacy .asmx service I’m dealing with. (this is a Backbone.js standard member)
  • “clear” – removes the view this model is associated with from the page. (this is a Backbone.js standard member)
  • “updateData” – ajax success call back function that takes the response and populates the model with updated data.

The Views

We have two views: “JobStatusView” and “AppView”.  AppView is what it sounds like – the main app view.  It houses the controls used to add a “JobStatusView” to the page. 

$(function() {
    window.JobStatusView = Backbone.View.extend({

        tagName: "li",

        className: "jobStatView",

        template: _.template($('#stat-template').html()),

        initialize: function(options) {
            _.bindAll(this, 'render', 'remove');
            this.model.bind('change', this.render);
            this.model.view = this;
        },

        render: function() {
            $(this.el).html(this.template(this.model.toJSON()));
            $(this.el).bind('dblclick', this.remove);
            return this;
        },

        remove: function() {
            $(this.el).remove();
            this.model.container.remove(this.model);
        }
    });

    window.AppView = Backbone.View.extend({

        el : $('#statusApp'),

        statList : $('#statusList'),

        initialize: function() {
            _.bindAll(this, 'addOne', 'updateData' ,'render', 'addAllJobs');
            this.model = new SqlJobStatusCollection();
        },

        addOne: function(jobName) {
            var mdl = new window.SqlJobStatus({ JobName: jobName});
            var view = new window.JobStatusView({ model: mdl });
            this.model.add(mdl);
            mdl.container = this.model;
            mdl.fetch();
            this.statList.append(view.render().el);
        },

        updateData: function() {
            for(i = 0; i < this.model.length; i++)
            {
                this.model.models[i].fetch(null);
            }
        },

        render: function() {
            var html = "<div id='choice-div'>" +
                           "<span>Select the job you want to query for:</span>" +
                           "<select id='selJob' name='jobName' >" +
                                "<option>Back to the Future</option>" +
                                "<option>Dances with Wolves</option>" +
                                "<option>Gladiator</option>" +
                                "<option>Primer</option>" +
                                "<option>Shaun of the Dead</option>" +
                                "<option>Stargate Continuum</option>" +
                            "</select>" +
                            "<input type='button' value='Add' onclick='window.appView.addOne($("#selJob").val());' />&nbsp;&nbsp;" +
                            "<input type='button' value='Add All Jobs' onclick='window.appView.addAllJobs();' />" +
                        "</div>";
            $(this.el).html(html);
        },

        addAllJobs: function() {
            $('#selJob option').each(function() {
                window.appView.addOne($(this).val());
            });
        }
    });
});

 

Let’s break down these two views:

JobStatusView (View for individual SQL Agent Job Status)

  • “tagName” – The DOM element tag name that will be created for this view.  (Note that Backbone.js allows you to specify an existing DOM element, OR your can provide a tagName value and it will create a DOM element of that type for you. (this is a Backbone.js standard member)
  • “className” – The CSS class name that will be applied to this DOM element. (this is a Backbone.js standard member)
  • “template” – the name of the view template that will be used.
  • “initialize” – called when the view is created.  I’m setting the “this” context of function members, binding the model “change” event to the render method of the view, and setting the model’s “view’” handle to this view. (this is a Backbone.js standard member)
  • “render” – I’m binding the template to the model, appending the resulting html to the DOM element, binding the double click event to the “remove” function. (this is a Backbone.js standard member)
  • “remove” – removes the view’s DOM element from the page, and triggers the removal of the model from its container as well. (this is a Backbone.js standard member)

AppView (The Application View)

  • “el” – represents the DOM element for this view (this is a Backbone.js standard member)
  • “statList” – a member I added that has a handle to the DOM element where JobStatusViews will be added.
  • “initialize” – called when the view is created.  In addition to setting the “this” context of the several functions, I’m setting the model the view is bound to. (this is a Backbone.js standard member)
  • “addOne” – handles adding a new JobStatusView to the statList element, which includes adding a SqlJobStatus model to this view’s collection.
  • “updateData” – simple iterates over the models in this views model collection and calls fetch on each one.
  • “render” – renders the view to the page.  (this is a Backbone.js standard member)
  • “addAllJobs” – iterates over the list of available jobs and adds models/views for each one.

          And Finally – the HTML Page to tie it all together

          
          <html>
          <head>
              <link rel="stylesheet" type="text/css" href="../Common/style.css" />
              <script type="text/javascript" src="../Common/project.js"></script>
              <script type="text/template" id="stat-template">
                  <fieldset class="<%=CssClass %>">
                      <legend>
                          <div class="legend-header">SQL Job Status</div>
                      </legend>
                      <table>
                          <tr>
                              <td class="label-header">Job Name:</td>
                              <td class="label-value"><span id="jobName"><%= JobName %></span></td>
                          </tr>
                          <tr>
                              <td class="label-header">Current Step:</td>
                              <td class="label-value"><span id="currentStep"><%= CurrentStep %></span></td>
                          </tr>
                          <tr>
                              <td class="label-header">Last Start Date:</td>
                              <td class="label-value"><span id="lastStartDate"><%= LastStartDate %></span></td>
                          </tr>
                          <tr>
                              <td class="label-header">Last End Date:</td>
                              <td class="label-value"><span id="lastEndDate"><%= LastEndDate %></span></td>
                          </tr>
                          <tr>
                              <td class="label-header">Last Run Time (minutes):</td>
                              <td class="label-value"><span id="lastDuration"><%= LastRunTime %></span></td>
                          </tr>
                          <tr>
                              <td class="label-header">Next Run Date:</td>
                              <td class="label-value"><span id="nextRunDate"><%= NextRunDate %></span></td>
                          </tr>
                          <tr>
                              <td class="label-header">Last Outcome Message:</td>
                              <td class="label-value"><span id="lastOutcomeMessage"><%= LastOutcomeMessage %></span></td>
                          </tr>
                      </table>
                  </fieldset>
              </script>
              <script type="text/javascript" src="underscore.js"></script>
              <script type="text/javascript" src="backbone.js"></script>
              <script type="text/javascript" src="../Common/json2.js"></script>
              <script type="text/javascript" src="../Common/jquery-1.4.2.js"></script>
              <script type="text/javascript" src="Views.js"></script>
              <script type="text/javascript" src="Models.js"></script>
              <script type="text/javascript">
                  $(function() {
                      window.appView = new window.AppView();
                      window.appView.render();
          
                      window.pollData = function() {
                          window.appView.updateData();
                      }
          
                      setInterval("window.pollData()", 2000);
                  });
              </script>
          
              
          
          
              <div id="statusApp" class="div-containerB"></div>
              <ul id="statusList"></ul>
          
          
          

           

          Oddly enough, the majority of the page consists of script elements.  Notice that we’re not only using them to load script dependencies, but we’ve also defined the view template for the JobStatusView in one.  In the last script element we’re creating the AppView instance, rendering it, setting up a polling function to retrieve data for any jobs being viewed at a regular interval.

          Viewing the Results

          When the page loads, it’s empty aside from this:

          image

          The above output is evidence that our AppView has rendered correctly.  If we were to select one of the jobs and click “Add”, we’d see something similar to this:

          image

          If we click the “Add All Jobs” button, views like the one above will be added to the page for all six available jobs.  You might have noticed that in our “updateData” function in the SqlJobStatus model conditionally set the “CssClass” based on the status of the job.  As a result, active jobs in the below screen shot are green:

          image

          Post Mortem

          I have mixed feelings about Backbone.js  – albeit based on this very limited exposure.  I think their approach to mapping controller actions to url fragments is brilliant.  Although I didn’t know when I started this experiment that Backbone.js is primarily designed to talk to RESTful services (and my example service is anything but), I think it’s the right direction and a good choice on their part.  The aspect of Backbone.js that I am least happy with is how Views are handled.  Emitting HTML – whether it be from a template, or in-line concatenated HTML (my example has both) – from a “render” method feels….heavy?  Too opinionated?  There were several moments as I built this example when I wondered why I wasn’t seeing my results on the screen – it was because I had forgotten to call “render”.  This is a tough hurdle to clear when you’re used to there being very little between you, your HTML and it being immediately rendered to the browser.

          This complaint wouldn’t rule out my consideration of Backbone.js for projects in the future.  I think the main thing to keep in mind is that it is an opinionated framework (much more so than Knockout.js, in my opinion) – designed to be a full stack for client-side MVC apps.  As long as that doesn’t conflict with your goals, I’d would definitely recommend checking it out.  The sample apps on the Backbone.js site are definitely worth browsing – these guys have done some amazing things.

          Check back soon for the second post in this series where I cover the same “example” application using Knockout.js…

           

          Tags: , , , , ,

          • A_Robson

            I honestly don't have mixed feeling about backbone (what a shock); embedding HTML in Javascript is a deal breaker for me. I don't like my concerns mixed that way, there are bound to be a lot of problems that crop up as a project matures in that kind of approach.

            Looking forward to your post(s) on Knockout.js :)

            • http://ashbylane.vox.com Jim Cowart

              Agreed – I'm not a fan of emitting (or embedding) any more HTML into JS than is absolutely necessary. This is one of the reasons the declarative binding approach of KO is so compelling. It feels (and looks) cleaner & helps separate the concerns.

              • Andrew Petersen

                I used Backbone for a small project at work. I think its strongest point is the auto-REST loading/saving, and if you're not going to use that… then it might not be a right fit. While yes, it has a structure it wants you to follow, you can also just eschew that and rewrite built-ins. But it does take a good amount of time to get used to what it wants you to do (or to even figure out HOW you should do things).

                However, I have to disagree with part of the comment about embedding HTML into JS. Obviously, we don't want these concerns mixed. But you don't have to embed your HTML in the window.AppView.render function, that's what templates are for. Also, after reading the second part of this series, I would definitely argue that data-bind=" this is basically javascript code " is no better in terms of separation!

                Thanks for writing these two articles, I enjoyed them.

                • Mosselman

                  Yes exactly thank you.

                  Templating is a great way for you to keep html outside of Backbone.

                  Another point of view I have is that views just ways to interact with the models and collections. You would create a view for html specific elements anyway. It will cost you far more time to create some sort of uber generalised View 'class' that works for every single type of html, than it will cost you to make a specific one.

          • Pingback: Knockout.js vs. ..

          • http://ashbylane.vox.com Jim Cowart

            Andrew – thanks so much for the comment! My preference would be to use templates to avoid any html-embedding if I were to move forward with a project using backbone. I purposefully made one of the render functions use concatenated html to reflect the frequency with which I saw other backbone examples take that approach. I definitely see your point in arguing that the Knockout approach is no better in separating the concerns, but I disagree primarily because most web developers are already used to, for example, hooking DOM events up to js handlers via element attributes – and this fits that paradigm closely. Another reason why it's less intrusive to me, admittedly, is that declarative bindings are the norm in WPF and Silverlight – and I spent a good amount of time in WPF at my last job. When you have views driven primarily by declarative code, attribute bindings seem to be a more natural fit (in that they are readable, and provide a concise way of saying "Hey – I'm hooking this view into your model *here*.") than sifting through the behavior of a imperative-based view to see what declarative elements get emitted (or are referenced, in the case of templates). Of course – that's just my opinion. :-) While I personally lean towards frameworks like Knockout, I'm impressed with what I've seen done with Backbone – and I agree, the auto-REST loading & saving (plus the history features) are strong points for Backbone.

          • kitd

            May be too late, but have you looked at AngularJS? You might consider adding it to the comparison list.

            • http://ifandelse.com Jim Cowart

              Thanks for the info – I will check it out!

          • http://twitter.com/abossard Andre Bossard

            I created an internal web-based push messaging app with backbone and I don’t see it as opinionated, as it leaves it up to you:
            - which template mechanism you use (or even *cough* inlining it)
            - if render() should be called from inside the controller (data update, init, …), or outside (dom ready)
            - if a controller even outputs html, or just plugs in several sub controllers