Tuesday, November 29, 2011

Default Backbone.js Route Revisited

‹prev | My Chain | next›

As I work through the last of the issues before the release of Recipes with Backbone, I make a quick post to keep the god of my chain appeased.

With regards to the route redirection with which I was fiddling last night, my co-author, Nick Gauthier suggested that I try an alternative approach. Specifically, my default route is currently manually navigating to the correct push state location for my App:
  var Routes = Backbone.Router.extend({
    // ...
    routes: {
      "": "setDefault",
      "page/:page": "page"
    },

    setDefault: function() {
      Backbone.history.navigate("#page/" + this.paginator.page());
    },
    // ...
  });
However, the original empty route prior to the redirect in setDefault remains in browser history (at least in some browsers). This means that the Back button is effectively broken—clicking Back will always hit the empty route in the browser history which redirects.

Nick's suggestion was to do away with the setDefault() router method. Instead, he suggests mapping the empty route directly to the page() route method:
  var Routes = Backbone.Router.extend({
    // ...
    routes: {
      "": "page",
      "page/:page": "page"
    },
    // ...
    page: function(page) {
      if (typeof(page) == "undefined") page = 0;
      this.paginator.page(page);
      this.paginator.render();
    }
  });
The only change I then need to make is to teach the page() method what to do when page is not set (e.g. in the URL page/2). As can be seen above, I set the default page to zero.

And that works. Even clicking the Back button keeps the URL and the pagination controls in sync:


So ultimately, the solution is to not attempt router redirection. Still, we have some decent workarounds in the book (in addition to the above).


I call it a day here and am going to get back to proof reading. So tired....


Day #221

Monday, November 28, 2011

Redirection in Backbone.js

‹prev | My Chain | next›

I made a bit of a mess of yesterday's Backbone.js router post. Several things conspired against me last night, not the least of which was my own ignorance. OK, so it was entirely my ignorance, you happy? Ahem.

Anyhow, I would ultimately like to come up with a reasonable strategy for redirection that Nick Gauthier and I can include in Recipes with Backbone. I can safely say that dedicated redirect routes do not work in Backbone. For instance, if I want to redirect my user to the next result in a set, a poor practice is to establish links along the lines of:
    <div id='navigation'>
      <a href='#previous'>Previous</a>
      <a href='#next'>Next</a>
    <div>
I can make that kinda/sorta work with the following routing code:
  var Routes = Backbone.Router.extend({
    initialize: function(options) {
      this.paginator = options.paginator;
      _.bindAll(this, 'next', 'previous', 'page');
    },

    routes: {
      "next": "next",
      "previous": "previous",
      "page/:page": "page"
    },

    next: function() {
      console.log("[next]");
      this.paginator.move(1).render();
      Backbone.history.navigate("#page/" + this.paginator.page());
    },

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

    page: function(page) {
      this.paginator.page(page);
      this.paginator.render();
    }
  });
The next() method tells the paginator view class to increment its internal page count and re-render (presumably with the correct page number). I then explicitly perform a "redirect", via Backbone.history.navigate() to #page/2.

It would not make any sense to leave the user on the #next resource—it is not a real resource, just a stopping point between real destinations. Besides, if the user attempted to bookmark the #next location, hilarity would ensure. And by "hilarity", I mean pain. For the user.

Anyhow, I say that this kinda/sorta works because the back button is broken with this redirection mechanism. If I am on page 2 and click "Back", I will hit the #next resource, which will send me onto page 3—exactly the opposite of where I want go.

So really, explicitly linking to redirection routes is a non-starter. I remove this solution. Instead, I focus on trying to generate custom events in my view to which the router can respond:
    var Paginator = Backbone.View.extend({
      // ...
      previous: function() {
        this.trigger('previous');
      },
      next: function() {
        this.trigger('next');
      },
      // ...
    });
Now, I can pass my paginator view to my router:
  var paginator = new Views.Paginator({el: root_el}).render();
  new Routes({paginator: paginator});
And my routes can then subscribe to the view's redirection events:
  var Routes = Backbone.Router.extend({
    initialize: function(options) {
      this.paginator = options.paginator;
      _.bindAll(this, 'next', 'previous', 'page');
      paginator.bind('next', this.next);
      paginator.bind('previous', this.previous);
    },

    next: function() {
      console.log("[next]");
      this.paginator.move(1).render();
      Backbone.history.navigate("#page/" + this.paginator.page());
    },

    previous: function() {
      console.log("[previous]");
      this.paginator.move(-1).render();
      Backbone.history.navigate("");
    },
    // ...
  });
Now, when I navigate with the "Next" and "Previous" controls from my view (that only generate events, but do not link to anything in the traditional sense), the controls stay in sync with the browser URL. Even click the Back button sends me to the correct URL (#page/1) with the correct page number rendering in the view:


The Back button is working here thanks to the definition of the #page/1 route:
  var Routes = Backbone.Router.extend({
    initialize: function(options) { /* ... */ },

    routes: {
      "page/:page": "page"
    },

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

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

    page: function(page) {
      this.paginator.page(page);
      this.paginator.render();
    }
  });
The only trouble that I run into with this solution is clicking all the way back to the first page. When I first enter, I am shown page zero:


But when I click the Back button, there is no matching default route, so the controls remain stuck on page one while the URL is sent back to the original, root URL:


One solution to this is that I can set a default route for my application. But again, if I do something like that, I end up in a redirection loop when trying to hit the Back button past the default route.

To get that working, I make a Backbone.history.navigate() call in my default route:
  var Routes = Backbone.Router.extend({
    //...
    routes: {
      "": "setDefault",
      "page/:page": "page"
    },

    setDefault: function() {
      Backbone.history.navigate("#page/" + this.paginator.page());
    },
    //...
  });
It turns out that does exactly what I was looking for. Rather than adding a new history entry on top of the default "" route, it replaces the original route with #page/0. Success!


Except it does not work in Firefox. In Firefox, the default route sticks around, even on page zero:


Bah! There may be a way to overcome this in Firefox, but of course, then there is IE. Double Bah!

I call it a day at this point. I think I have enough research information for Recipes with Backbone. I cannot quite offer a complete solution, but at least I know the extent to which the recipe applies. Good enough for a 1.0 edition.


Day #220

Browser History and Backbone.js Redirection

‹prev | My Chain | next›

Up tonight a little bit flipping with Backbone.js routes. In the upcoming Recipes with Backbone (which is due in lesS THAN TWO DAYS11!!!!! It's OK. I'm OK. Just breathe…), we have a recipe for router redirection which ends up a little confused.

Specifically, we start the recipe discussing the benefits of server-side redirection, which does not contribute to browser history, and comparing that to clien-side redirection, which does. We somewhat imply that Backbone redirection can achieve similar results, but then never go back to verify this claim (or even mention it again).

So my goal tonight is to either generate some backing material for said recipe or remove that claim (the recipe is already pretty awesome without it). For this, I will not be using my Calendar application. Rather, I think that a smaller, simple Backbone application will suffice.

So I trigger next /previous events in a simple view. With those events in the router, I navigate forward / backward:
  var Routes = Backbone.Router.extend({
    initialize: function(options) {
      this.paginator = options.paginator;
      _.bindAll(this, 'next', 'previous');
      paginator.bind('next', this.next);
      paginator.bind('previous', this.previous);
    }
What I find when I do this is that after clicking next twice I am on page 2:


But clicking the back button leaves me stuck on page 2 — it won't navigate me back.

If I add manual links:
    <h1>Router Redirection</h1>
    <div id="results">testing</div>
    <div id="paginator"></div>
    <div id='manual'>
      <a href='#previous'>Previous</a>
      <a href='#next'>Next</a>
    <div>
...And try visiting the routes directly, I am again able to get to page 2:


So that I am not stuck on the /appointments/#next resource, I manually reset the current URL, effectively redirecting, with the following router method:
    next: function() {
      console.log("[next]");
      this.paginator.move(1).render();
      Backbone.history.navigate("");
    },
But now, when I click the back button, and I am taken back to the next resource, which send me on to page 3!


Ugh. I still do not know that I have a definitive answer to how client-side redirection might work in Backbone. All I know right now is that I cannot make it work. So I suppose I am leaning toward removing that section from the recipe. Still, I will sleep on it for now. Maybe I can come up with something tomorrow.


Day #219

Sunday, November 27, 2011

BDDing Backbone with Sinon.js Fake Servers

‹prev | My Chain | next›

Up today, I continue my efforts to BDD a simple appointment list view in my calendar Backbone.js application. The only significant remaining feature is more of a bug—when switching from month view to list view, the list view is retaining the collection filter that only requests appointment from a single month.

In jasmine-speak, I might say:
    it("requests all appointments (forgets previous filter)");
Since I am faking the request to the server, how I go about expressing my expectations is going to be a little tricky. Tricky, perhaps, but I do not have many options. I am forced to spy on the AJAX request that goes out. Or maybe not...

In my setup, I am using sinon.js to stub out AJAX requests to anything under the "/appointments" URL space:
  describe("list view", function() {
    beforeEach(function() {
      // ...
      server.respondWith('GET', /\/appointments/,
         [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
    it("requests all appointments (forgets previous filter)");
  });
If I change my setup to only respond when the "/appointments" URL is called (i.e. without filtering query parameters), then I ought get the desired result:
  describe("list view", function() {
    beforeEach(function() {
      // ...
      server.respondWith('GET', '/appointments',
         [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
    it("requests all appointments (forgets previous filter)");
  });
In fact, I do get my desired result because my earlier list view tests fail:

To make those pass again, I need my application view to clear my collection's date and for the collection to not pass a date query parameter when the date is not set:
    var Application = Backbone.View.extend({
      // ...
      setListView: function() {
        this.view = 'list';
        this.collection.setDate();
        this.collection.fetch();
        return this.render();
      },
      // ...
    });

    var Appointments = Backbone.Collection.extend({
      // ...
      fetch: function(options) {
        options || (options = {});

        var data = (options.data || {});
        if (this.date) options.data = {date: this.date};

        // ...
        return Backbone.Collection.prototype.fetch.call(this, options);
      },
      // ...
    });
With that, I have all of my tests passing:

And, all appointments are now shows on the list view:

With that out of the way, I am ready to tackle a few things that need verifying in Recipes with Backbone. Tomorrow. For now, I have much proof-reading ahead of me.



Day #218

Saturday, November 26, 2011

BDDing Backbone.js with Jasmine (Day 2)

‹prev | My Chain | next›

After my jasmine-fueled BDD session last night, it is time to poke my head up to see how it looks. The verdict is that I need to drive a few more things:


So, from top-to-bottom, I need to remove the sub-title date, need more appointments (those are only from November, it should include all appointments, the appointment titles should be defined, and the next / previous navigation elements should be gone.

Removing the subtitle and the navigation elements should be the responsibility of the Month View, not my new list view. So I add new specs to my Month view:

  describe("month view", function() {
    // ...
    describe("leaving", function() {
      it("removes its subtitle");
      it("removes navigation elements");
    });
  });
I already have spec setup code to create a Month view. For the setup in this spec, I need to also navigate to the list view, which should remove the subtitle and navigation elements:
    describe("leaving", function() {
      beforeEach(function() {
        window.calendar = new Cal($('#calendar'));
        Backbone.history.navigate('', true);
        Backbone.history.navigate('#list', true);
        // ....
      });
      // ...
    });
With the setup navigating to the list view, I can define my expectation:
      it("removes its subtitle", function() {
        expect($('h1')).not.toHaveText(new RegExp(year));
      });
Which fails:


To make that pass, I simply need to have a higher level view (the application view in this case), tell the Title View to remove itself when switching to the List View:
    var Application = Backbone.View.extend({
      // ...
      render: function() {
        if (this.view == 'list') {
          view = new CalendarList({collection: this.collection});
          this.title_view.remove();
        }
        else { /* ... */ }

        $(this.el).html(view.render().el);
      },
      // ...
    });
And now I have reached the green:


I write a similar spec for my navigation controls, except in this case I expect the controls to be hidden:
      it("removes navigation elements", function() {
        expect($('.previous')).not.toBeVisible();
      });
To make that pass, I again make the higher level view responsible for telling the lower order view to remove itself.

The last thing that I try to solve tonight is the mystery "unknown" titles. For that, I know that my fake (sinon.js) server response includes an appointment titled "Appt 001". So I create an expectation that the appointment list view should include that title:
  describe("list view", function() {
    var couch_doc1 = { "title": "Appt 001", /* ... */ }
    // ...
      , doc_list = { "rows": [ {"value": couch_doc2}, /* ... */ ] };

    // ...
    it("displays appointment titles", function() {
      expect($('#appointment-list')).toHaveText(/Appt 001/);
    });
  });
To make this pass, I simply have to recall that accessing Backbone model attributes is done via a get() method, not by accessing attributes directly on the model:
    var CalendarList = Backbone.View.extend({
      // ...
      render: function() {
        $(this.el).html(
          '<h2>Appointment List</h2>' +
          '<ol id="appointment-list">' +
          this.collection.reduce(function(memo, appointment) {
            return memo + '<li>' + appointment.title + '</li>';
          }, "") +
          '<ol>'

        );
        return this;
      }
    });
After changing that to appointment.get("title"), I have my green test. And quick sanity check reveals that I fixed most of my problems:

I call it a night here. Up tomorrow: I need to do some more experimenting with the router to support a recipe in Recipes with Backbone.


Day #217

Friday, November 25, 2011

BDDing New Backbone.js Views with Jasmine

‹prev | My Chain | next›

I believe that I have more or less finished exploring testing Backbone.js with jasmine. Ready to tackle something new, I think I will try adding a different view to my calendar application. And what better way to add a new feature that driving it with BDD?

And lo! I know just the tool to BDD that feature. Looks like I am not quite done with Jasmine after all…

For my month view, I have just been stubbing (with sinon.js) server lookups to return a single appointment. For the list view, I think I ought to start by returning three appointments:
  describe("list view", function() {
    var couch_doc1 = {
          "_id": "1",
          "_rev": "1-1111",
          "title": "Appt 001",
          "startDate": "2011-10-01"
          // ...
        }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = {
          "total_rows": 3,
          "rows": [
            {"value": couch_doc2},
            {"value": couch_doc1},
            {"value": couch_doc3} ]
        };
My backend is, of course, CouchDB—hence the structure of the document list that will be returned. I am intentionally mucking with the order of the documents because I am anticipating sorting them in the front-end.

With the appointment documents ready, I write my spec setup, which does the usual of creating a new instance of my Backbone Cal application. It also stubs out server calls to return the document list from the main describe("list view", ...) block:
  describe("list view", function() {
    var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }

    beforeEach(function() {
      window.calendar = new Cal($('#calendar'));
      Backbone.history.navigate('#list', true);

      // populate appointments for this month
      server.respondWith('GET', /\/appointments/,
        [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
  });
The only real difference between this setup and the setup for my month view is that I am navigating to '#list' rather than '#month'.

On that list page, I expect to see 3 appointments. Or, in jasmine-ese:
  describe("list view", function() {
    var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }

    beforeEach(function() { /* ... */ });

    it("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
  });
When I load the new "list view" specs in the browser, I get the expected failure:


So my next question is, what do I need to do to change the failure message or make it pass? Simply asking the question makes me realize I have gotten ahead of myself. To do any of this, I am going to need a "list view". I do not even have a route that responds to such a thing, let alone a view.

So, I take a step back and mark this test pending (for now) and write a more appropriate first step:
  describe("list view", function() {
    var /* couch_doc1, couch_doc2, couch_doc3, doc_list */
    beforeEach(function() { /* ... */ });

    it("routes to the list view", function() {
      expect($('h1')).toHaveText(/Appointment List/);
    });

    xit("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
  });
With that, I have a smaller failure:


To be sure, I could make an even smaller first failure by testing the routing itself. I am jumping ahead of myself a little—hopefully I will not get burned. It turns out to require quite a few changes simply to get the new view to render. I have to add a new route, which calls a new method on my application view, which creates a new view type:
  var Application = Backbone.View.extend({
    // ...
    setListView: function() {
      this.view = 'list';
      return this.render();
    },
    render: function() {
      var view;

      if (this.view == 'list') {
        view = new CalendarList();
      }
      else {
        var date = this.collection.getDate();
        view = new CalendarMonth({date: date});
      }

      $(this.el).html(view.render().el);
    },
    // ...
  });

  var CalendarList = Backbone.View.extend({
    render: function() {
      $(this.el).html("<h2>Appointment List</h2>");
      return this;
    }
  });
After all of that, I have my first passing test:


That took a bit more effort than anticipated. I am pretty sure I have introduced a memory leak in there. I know that I am violating at least one Recipes with Backbone recipe ("Evented Routers"). But none of that matters: (1) get it done, (2) do it right, (3) optimize. I have reached #1 and that is good enough for now. I will worry about #2 and #3 another day if needed (and with the benefit of more tests).

For now, I would like to get my original test passing:
    it("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
In the spirit of "get it done", I pull out an Underscore.js reduce():
    var CalendarList = Backbone.View.extend({
      initialize: function(options) {
        options.collection.bind('reset', this.render, this);
      },
      render: function() {
        $(this.el).html(
          '<h2>Appointment List</h2>' +
          '<ol id="appointment-list">' +
          this.collection.reduce(function(memo, appointment) {
            return memo + '<li>' + appointment.title + '</li>';
          }, "") +
          '<ol>'

        );
        return this;
      }
    });
To be sure, this is a very rudimentary listing of appointments. In the final version, I will certainly need to show more than a list of static titles. But it does enough to make my test pass:


Since it works, that will serve as a stopping point tonight.


Day #116

Thursday, November 24, 2011

Avoiding a Backbone.js + Jasmine Rabbit Hole

‹prev | My Chain | next›

Ugh. While re-organizing my jasmine tests yesterday, I noticed a disappointing hole in testing coverage for my Backbone.js calendar application. Specifically, I am only testing my default route, not navigated routes.


My first instinct was to use the routing specs to test the default behavior (possibly adding one or two more simple descriptions of default behavior). I could then re-purpose the remaining specs to describe specific calendar / appointment behavior. Unfortunately, too much broke when I tried that. Too many tests failed and my first attempts at fixing them only made matters worse.

Ultimately, I think that is how I would like to organize my tests, but to get there, I will need to write some (hopefully) temporary specs to help me transition to that point. So first, I revert my failed refactoring attempt. Then I move all of my existing initial view specs into a self-contained context:
  describe("the initial view", function() {
    beforeEach(function() { /* ... */ });

    describe("the page title", function() { /* ... */ });

    // ...
  });
I am sure to include the previous setup (including HTTP server stubs) in this new scope. This will prevent any context meant for the default navigation from spilling over into my new context.

As for that new context, my setup looks quite similar to what I have done previously except that the one record returned from the /appointments resource needs to be from a specific date in the past:
  describe("navigated view", function() {
    beforeEach(function() {
      // Create the application
      window.calendar = new Cal($('#calendar'));

      // Manually travel back in time to 1999
      Backbone.history.navigate('#month/1999-12', true);

      // doc_list contains one record, couch_doc, which should
      // now be from 1999:
      couch_doc['startDate'] = "1999-10-31";
      couch_doc['title'] = "Party";

      // populate appointments for this month
      server.respondWith('GET', /\/appointments/,
        [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
  });
So my setup now creates an instance of my Backbone app, performs a manual navigation to the appropriate month (December 1999 in this case), and establishes the requisite sinon.js server stubs for this date.

As for the actual test, it should be enough to know that the "Party" appointment is listed in my month view:
  describe("navigated view", function() {
    beforeEach(function() { /* ... */ });

    it("should have an appointment", function() {
       expect($('#calendar')).toHaveText(/Party/);
    });
  });
And just like that, it passes:


Ugh.

I mean yay!

I was really headed down quite the rabbit hole before I took this little step back. It is hard to say exactly why things were going so wrong for me. In part, it was because I was testing the wrong thing. Now I am very explicitly testing just the result of manually navigating to a specific month (as a bookmarked push-state URL might do). When I tried to re-purpose my older tests, I was loading the original page, then navigating off and getting double-loading weirdness.

I do think that my app's blank state could be in better shape, but I am not going to worry about that now. If nothing else, my new test verifies that it should not matter -- that both the default and bookmarked start pages work as expected.


Day #215

Wednesday, November 23, 2011

Testing Backbone.js Routing with Jasmine

‹prev | My Chain | next›

Yesterday I had me a big breakthrough in my Backbone.js testing with jasmine. By replacing one, tiny snippet of non-idiomatic routing code with the "backbone way" equivalent, I magically eliminated a number of annoyances.

In particular, I now have tests failing when I intentionally break my application's routing. Unfortunately, the failure message is non-obvious. The failure indicates that appointments are not being populated on my calendar application. In fact, they are not being populated because the routing has failed, but it will take a bit of digging each time I cause this failure.

So today, I hope to test things a little more directly. Given the I am creating the application in my spec setup:
  beforeEach(function() {
    // ...
    window.calendar = new Cal($('#calendar'));
    // ...
  });
Then, in my tests, I can check to see that the application auto-redirects to the current month:
describe("routing", function() {
    it("defaults to the current month", function() {
      var today = new Date()
        , year = today.getFullYear()
        , m = today.getMonth() + 1
        , month = m<10 ? '0'+m : m;

      expect(Backbone.history.getFragment())
        .toEqual("month/" + year + "-" + month);
    });
  });
That passes, but, when I remove the true second argument to Backbone.history.navigate() in the application's default route:
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month); //, true);
    },
    // ...
  });
Unfortunately, that does not fail. Ah, of course... I am testing that the default route does something. What I try to break is the subsequent triggering the appropriate route. So the default route is firing, but is not triggering the applicable route.

For that, I need to add a secondary test to verify that default route sets the application date in addition to redirecting:
  describe("routing", function() {
    it("sets the date of the appointment collection", function() {
      var appointments = window.calendar.appointments;
      expect(appointments.getDate())
         .toEqual("2011-11");
    });
  });
With, that, I get my desired failure:


I can then replace the true argument to ensure the subsequent route is triggered and now my test passes:


If you cannot make a test fail by changing one line (or even one word), then the test is useless. I was a bit worried at first about the initial passing test. Once I realized I was testing two different things (the route firing and the route triggering a subsequent route), I got a failing test. I now feel much better about my test suite.

Failure: it's a good thing when testing.


Day #214

Tuesday, November 22, 2011

Jasmine is Hard when Writing Bad Backbone.js Code

‹prev | My Chain | next›

By way of quick follow up to yesterday's Backbone.js testing post, an astute reader pointed out that I had failed to read the documentation on Backbone.history.navigate(). It seems that it takes two arguments—if the second is true, then the route being navigated will trigger.

Thus I can replace last night's default route:
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month);
      this.setMonth(month);
    },
    // ...
  });
Which can be replaced with a version that removes the explicit call to setMonth() and adds true as a second argument to navgiate():
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month, true);
    },
    // ...
  });
This works by virtue of a month-matcher route, which is fired after navigating to the month route in combination with the true second argument to navigate():
  var Routes = Backbone.Router.extend({
    routes: {
     // ...
      "month/:date": "setMonth"
    },
    // ...
    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
And that works. When I open the root URL for my app, the default route kicks in, navigates to the current month and my appointments are on the calendar:


And if I remove the true second argument, my calendar now lacks appointments because nothing triggers the appropriate route:


The thing is, that my specs continue to pass:

This is because my tests inject a different, testing route and then manually does the route's job—setting the current date:
  beforeEach(function() {
    // ...
    window.calendar = new Cal($('#calendar'), {
      defaultRoute: function() {
        console.log("[defaultRoute] NOP");
      }
    });

    window.calendar.application.setDate("2011-11");
    // ...
  });
Now, to a certain extent, I do not have much choice. I have to explicitly set the date in my tests otherwise my November based tests are going to fail once December rolls around. But on the other hand, how can I get my tests to fail when routing is broken?

Ah, but wait a second. I have been building on a number of assumptions that are probably incorrect. Instead of using Backbone.history.navigate() to move my application around, I have been manually setting window.location. I have already found that some things are easier if I use navigate(). Perhaps testing is similarly easier.

So, I remove the defaultRoute injection from my jasmine test setup:
  beforeEach(function() {
    // ...
    window.calendar = new Cal($('#calendar'));
    Backbone.history.loadUrl();
    // ...
  });
(I do need to read-add the manual Backbone.history.loadUrl() between runs per http://japhr.blogspot.com/2011/11/backbonejs-hash-tag-urls-with-jasmine.html)

Then, in my application code, I remove the true second argument to navigate():
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month);
    },

    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
With that, I am failing again:

Most importantly here, is not that I have failing specs, but that the console output includes logging statements for the default route being called, but not a subsequent logging message from setMonth(). If I re-add the true second argument to navigate():
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month, true);
    },

    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
Then my Jasmine tests pass and my console logs output from both the default route and the subsequent setMonth() triggered route:


Nice. That was a big point of frustration in my Backbone testing efforts. As usual, had I been doing things idiomatically, life would have been much easier. Much thanks to Nick Gauthier (my Recipes with Backbone co-author) and Patrick (in last night's comments) for showing me the way.

I could still ask for a more obvious failure in my tests than I have here, so I think tomorrow I will try to implement a routing-only test. That could very well be the end of my testing exploration.


Day #213

Monday, November 21, 2011

Don't Set window.location in Backbone.js

‹prev | My Chain | next›

Up today: a quick follow up to the post from yesterday about injecting a default route for testing purposes.

My Recipes with Backbone co-author, Nick Gauthier, suggested that my routing leaves a little to be desired. Specifically, rather than manually manipulating window.location, I should be using Backbone.history.navigate().

I should know better by now, so I give it a try:
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month);
    },
    // ...
  });
Hopefully that won't break too much. And by "break", I mean expose related bad assumptions on my part. Unfortunately, when I load that back up in my browser, I now get an empty calendar with no appointments:


Back when I was setting window.location, I could rely on my month route kicking in after I my new location matched the month route:
  var Routes = Backbone.Router.extend({
    routes: {
      // ...
      "month/:date": "setMonth"
    },
    // ...
    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
Now, it seems that I have to do it manually:
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month);
      this.setMonth(month);
    },
Thankfully, by re-using a parameterized route, not too much need change. And with that, I have my calendar populated correctly with appointments:


Best of all, my specs still pass:


Thanks, Nick! That feels much better. Up tomorrow: I will follow-up to see if I can remove any code as a result of this change.


Day #212

Sunday, November 20, 2011

Injecting Test Routes into Backbone.js

‹prev | My Chain | next›

Yesterday I was able to get my standalone Jasmine specs passing even when Backbone.js routing was active. In order to do so, I had to make the routing class a global class rather being entirely encapsulated inside my application.

Global classes in Javascript are still global variables:
window.Routes = Backbone.Router.extend({ /* ... */ });
Defining classes globally in other languages might be standard operating procedure, but in Javascript the global variable thing feels like a bad idea. So I revert that change:
➜  calendar git:(jasmine-standalone) git revert 8f79d0d

Revert "Make routes global so that they can be tested / spied upon."

Globals are a bad idea.

This reverts commit 8f79d0d20875568f5fdacdade1ec5c39c6d532a0.
My goal remains the same as yesterday. Specifically, I would like to be able to run my application under test without the routing kicking in to send the application off to places that it should not go. This is especially problematic when running filesystem-based / standalone specs.

So instead of trying to hook into the router from my specs as I did yesterday, today I try to make my code slightly spec-aware.

My fist attempt is to change the setDefault() router method to perform a check for the jasmine variable:
  var Routes = Backbone.Router.extend({
    routes: {
      "": "setDefault",
      // ...
    },

    setDefault: function() {
      var env = (typeof('jasmine') == 'undefined') ? 'Production' : 'Test';
      return this['setDefault' + env]();
    },

    setDefaultProduction: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      window.location = '/#month/' + month;
    },

    setDefaultTest: function() {
      console.log("[setDefaultTest]");
    },
    //...
  });
If jasmine is not present, the router can call the usual routing method, now renamed as setDefaultProduction(). If jasmine is defined, then the routing object will invoke the setDefaultTest() method, which is a simple no-op.

And that works:

That works, but, wow, do I dislike production code that serves no purpose other than to support testing. I can do a bit better.

Instead of explicitly tying my application code to testing code, I can provide a mechanism for the test to inject a default route. To support this, I add a second options argument for my Backbone application's constructor:
window.Cal = function(root_el, options) {
  // ...
  var defaultRoute = (options || {}).defaultRoute;

  var Routes = Backbone.Router.extend({
    // ...
    routes: {
      "": "setDefault",
      // ...
    },

    setDefault: function() {
      if (typeof(defaultRoute) == "function") {
        return defaultRoute.call(this);
      }
      
      return this.setDefaultProduction();
    },
    //...
  });
};
If that options argument contains a "defaultRoute" attribute, I set it to a local defaultRoute variable. Then, when my router tries to call setDefault() I can add a guard clause that will call the provided routing method.

Back in my test setup, I can then inject a NOP default route:
  beforeEach(function() {
    // ...
    window.calendar = new Cal($('#calendar'), {
      defaultRoute: function() {
        console.log("[defaultRoute] NOP");
      }
    });
With that, I have my tests passing again. I have introduced only a little new code and none of it explicitly references my testing code. I do have a new construct that is only useful for test support at the moment. But, better a slight violation of YAGNI than including testing code in my production code.


Day #210

Saturday, November 19, 2011

Standalone Route Testing in Backbone.js

‹prev | My Chain | next›

Last night I was able to convert my jasmine test suite from the jasmine ruby gem to a standalone (loaded directly from the filesystem):

(note the file:///home/cstrom/repos/calendar/spec/SpecRunner.html URL)

I am pleasantly surprised that I can still simulate network operations with sinon.js even though I am no longer loading from the jasmine ruby gem's server. Nearly all of those specs include something along the lines of, if the Backbone.js application POSTs to the server with this data, then respond with that data. And it all works.

Except... in order to get things to work, I had to disable my default route in the Backbone application itself:
  var Routes = Backbone.Router.extend({
    routes: {
      "": "setDefault",
      // ...
    },

    setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      //window.location = '/#month/' + month;
    },
    // ...
  });
If I uncomment that line and reload my spec, the default route kicks in and redirects the browser to file:///#month/2011-11:


If that were an HTTP URL, then the window.location would include the hostname in the URL, meaning that the new window.location would be something like http://example.com/#2011-11. That would work just fine with push state and the Backbone router. Since I am file based, however, window.location is empty and window.location gets assigned to /#2011-11, which Firefox interprets as the root directory on my machine.

I can see working around this in a couple of ways. First, my spec could assign a window.location that would by-pass the default route. Second, I might alter my code to skip the default route if window.location is empty (e.g. the application is running under test).

Today, however, I would like to see if I can stub out that call to Router#setDate. To do so, I would need to use the spyOn() method from jasmine. Something along the lines of:
spyOn(Routes.prototype, 'setDefault').andCallFake(function() {
  console.log("fake Routes#setDefault");
  window.calendar.application.setDate("2011-11");
});
That establishes a spy on the setDefault() function of the Routes prototype. In other words, it spies on the setDefault() method of any Routes object—like the one that is used in my application. When that spy sees the setDefault() method called, it performs the application.setDate() call that would have been applied by the Route. So I am doing what the route would normally do—pretending to be the route, but without setting window.location to a potentially bogus file-based URL.

Except that does not work. I am encapsulating the router and everything else in my application inside my namespaced application:
window.Cal = function(root_el) {
  var Models = (function() {
    // ...
    return {Appointment: Appointment};
  })();

  var Collections = (function() { /* ... */ })();

  var Views = (function() { /* ... */ })();

  var Routes = Backbone.Router.extend({ /* ... */  });

  // Initialize the app and routes
  // ...
  new Routes({application: application});
  try {
    Backbone.history.start();
  } catch (x) {
    console.log(x);
  }

  return {
    Models: Models,
    Collections: Collections,
    Views: Views,
    Helpers: Helpers,
    appointments: appointments,
    application: application
  };
};
Neither the Routes class nor the instance are returned as properties of my Cal application object. Even if the instance were returned, it would be too late to spy on it—the instance would have already fired up the default route.

Similarly, I cannot export the Routes class as window.Routes because it would not be defined until the application is instantiated. Creating a new instance of the application class would create the routing instance as well as defined the class. So again, I cannot spy on the route that way.

The only avenue that I see open to me is to move the routing definition outside of the application class definition:

window.Cal = function(root_el) {
  // ...

  new Routes({application: application});

  // ...
};

window.Routes = Backbone.Router.extend({ /* ... */  });
With that, I can spy on the setDefault() function of the Routes prototype. However, I cannot invoke the application view's setDate() method:
  beforeEach(function() {
    // ...

    spyOn(Routes.prototype, 'setDefault').andCallFake(function() {
      console.log("fake Routes#setDefault");
      window.calendar.application.setDate("2011-11");})();
    });

    window.calendar = new Cal($('#calendar'));

    // ...
  });
If I run my specs like that, I received an undefined "calendar" error:


This is because my calendar variable is not assigned until the new Cal instance is defined. Unfortunately, before it is completely instantiated, the routing is instantiated, which prematurely fires the Routing spy.

So this leads to a second compromise, which is to do nothing when the spy sees a setDefault() call. I can then explicitly call application.setDate() after the calendar Backbone application is instantiated (and after the routing is ready):
    spyOn(Routes.prototype, 'setDefault');

    window.calendar = new Cal($('#calendar'));
    window.calendar.application.setDate("2011-11");
With that, I have my test passing again:


I am none too convinced that this is a good approach. It is nice to know that it can be done, but defining the routes as a global variable just to support testing feels wrong. I may simply be probing the point at which a Backbone application needs to move from standalone specs to a jasmine server. Still, I have other options for how to deal with this. I will pick back up tomorrow exploring those.


Day #210

Friday, November 18, 2011

Testing Backbone.js with Standalone Jasmine

‹prev | My Chain | next›

Up tonight, I think I would like to play around a bit with "standalone" jasmine specs for my Backbone.js application. For the past week or so, I have refactored my tests and application to the point that I do not need fixture pages. Tonight I wonder if I can do away with the testing server from jasmine ruby gem.

I grab the ZIP file from http://pivotal.github.com/jasmine/download.html and extract it into my spec directory:
./lib/jasmine-1.1.0/MIT.LICENSE
./lib/jasmine-1.1.0/jasmine-html.js
./lib/jasmine-1.1.0/jasmine_favicon.png
./lib/jasmine-1.1.0/jasmine.css
./lib/jasmine-1.1.0/jasmine.js
Also in my spec directory, I have my spec file (contains the actual tests), some helper testing libraries that I have been using and a web page to load everything into:
./HomepageSpec.js
./helpers/jasmine-jquery-1.3.1.js
./helpers/sinon-1.1.1.js
./SpecRunner.html
The SpecRunner.html is adopted from the one in jasmine-core. The only changes that I make to it are linking in the library files (backbone, jQuery, etc.), test helper libraries, the actual test file and my Backbone application:
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.1.0/jasmine.css">
  <script type="text/javascript" src="lib/jasmine-1.1.0/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-1.1.0/jasmine-html.js"></script>

  <!-- Library files -->
  <script type="text/javascript" src="../public/javascripts/jquery.min.js"></script>
  <script type="text/javascript" src="../public/javascripts/jquery-ui.min.js"></script>
  <script type="text/javascript" src="../public/javascripts/underscore.js"></script>
  <script type="text/javascript" src="../public/javascripts/backbone.js"></script>

  <!-- Test Helpers -->
  <script type="text/javascript" src="helpers/jasmine-jquery-1.3.1.js"></script>
  <script type="text/javascript" src="helpers/sinon-1.1.1.js"></script>

  <!-- include spec files here... -->
  <script type="text/javascript" src="HomepageSpec.js"></script>

  <!-- include source files here... -->
  <script type="text/javascript" src="../public/javascripts/calendar.js"></script>
When I load up file:///home/cstrom/repos/calendar/spec/SpecRunner.html, I find that the browser gets redirected to a non-existent hash-tag on the file system. This is due to my default Backbone route:
  var Routes = Backbone.Router.extend({
    routes: {
      "": "setDefault",
      "month/:date": "setMonth"
    },

    setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      window.location = '/#month/' + month;
    },
    // ...
  });
For the time being, I comment out the Router initialization:
  // new Routes({application: application});
  // try {
  //   Backbone.history.start();
  // } catch (x) {
  //   console.log(x);
  // }
If I can get standalone working, I will revisit the routing.

Unfortunately, almost none of the specs are working:


It takes me a while to track down my issue, but it was merely how I was setting the application date in relation to a sinon.js fake response:
beforeEach(function() {
    // stub XHR requests with sinon.js
    server = sinon.fakeServer.create();

    $('body').append('<div id="calendar"/>');
    window.calendar = new Cal($('#calendar'));

    // populate appointments for this month
    server.respondWith('GET', /\/appointments/,
      [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
    server.respond();

    window.calendar.application.setDate("2011-11");
  });
Here, I am telling the server to respond to a GET request for the list of appointments with a JSON representation of the document list. The problem is that I am instructing the response to be sent before I trigger a fetch() with my call to the application's setDate() method.

The solution is easy enough. I simply move the setDate() call to immediately before the fake response is sent:
beforeEach(function() {
    window.calendar.application.setDate("2011-11");

    // populate appointments for this month
    server.respondWith('GET', /\/appointments/,
      [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
    server.respond();
  });
With that, I have all of my specs passing again:

That is pretty darn nice. Thanks in part to sinon.js, I have a very solid test suite over my application. It is not quite end-to-end, but as long as the server response does not change on me, I have the next best thing. Best of all, my testing setup has been reduced to a single file that can be loaded directly by the browser. To be sure, the ruby gem server has advantages, but lightweight testing like this is just nifty.

The only real drawback was the difficulty in tracking down that lack-of-response from sinon.js. Unlike the server based sinon.js setup, this file based setup did not return a default 404 error. The response just seemed to be swallowed silently.

Then again, I may just be too tired to thing straight. I think I will call it a night here and will pick back up with this tomorrow.

Day #209

Thursday, November 17, 2011

Backbone.js Dependency Injection Makes Testing Easy

‹prev | My Chain | next›

At one point I had my entire Backbone.js application (plus some required, supporting HTML) all in a single web page. It was quite huge.

Having worked with the code quite a bit recently, the entire contents of the <body> tag is now:
<h1>
  Funky Calendar
  <span class="year-and-month"></span>
</h1>

<div id="calendar"></div>

<script>
$(function() {
  window.calendar = new Cal($('#calendar'));
});
</script>
The actual Backbone code (Cal) is now defined entirely in a separate calendar.js file. That source file is loaded via <script> tag along with backbone.js and other Javascript dependencies.

What I hope this means in practice is that I can do away with some of the testing complexity that I have in place. Until now, I have needed to generate a testing fixture from the web page (since it defined so much of what was critical to running the app). For that, I had a very simple expresso test that ran my express.js server and dumped the homepage to the fixture directory. Then, in my jasmine test, I loaded that fixture into the DOM for testing with jasmine-jquery's loadFixtures().

Now, I need only a DOM element to which I can attach my application:
new Cal($('#calendar'));
So, I replace my fixture loading:
  beforeEach(function() {
    // ...
    loadFixtures('homepage.html');
    // ...
  });
Instead, I create a DOM element and inject it into my Backbone application:
  beforeEach(function() {
    // ...
    $('body').append('<div id="calendar"/>');
    window.calendar = new Cal($('#calendar'));
    // ...
  });
I do have to remember to remove that element after each test:
  afterEach(function() {
    $('#calendar').remove();
    $('#calendar-navigation').remove();
  });
Amazingly... it just works:

Without my fixture data, I am tempted to remove jasmine-jquery. I think better of that idea because I really like the toHaveText() matchers:
  describe("appointments", function() {
    it("populates the calendar with appointments", function() {
      expect($('#' + fifteenth)).toHaveText(/Get Funky/);
    });
  });
That was unexpectedly easy. It kinda make me wonder why I did not avoid fixtures in the first place. Ah well, lesson learned.


Day #208

Wednesday, November 16, 2011

Fixture Phantom Backbone.js Views

‹prev | My Chain | next›

Last night I removed jQuery UI dialogs from the web page of my Backbone.js application. Instead, I move the necessary DOM into the application itself. This has the advantage of decoupling my application from my web page. This should have the side-benefit of making testing easier since I will no longer have to load a fixture of the web page with each test.

It should make it easier, except after last night, two of my jasmine tests are now failing:



The first failure is telling me that my dialog is no longer showing. The second tells me that, even if it were showing, it would not contain the correct title. Ugh.

The add appointment dialog in my real application is definitely working, even after repeated opens:


Since it worksforme, I suspect something going wrong with my tests—or at least my expectations of how the tests should work. Since my tests have not changed, however, I first check out my code changes. After rooting through the changes and some well-placed debugger statements, I realize that my add-appointment dialog is sticking around between specs.

This is caused by how I add the dialog HTML to the DOM:
    var AppointmentAdd = new (Backbone.View.extend({
      initialize: function() {
        this.ensureDom();
        // ...
      },
      ensureDom: function() {
        if ($('#calendar-add-appointment').length > 0) return;

        // $('#calendar-add-appointment') does not exist, so create and populate it here
      }
      // ...
    });
In between specs, I am closing dialog windows:
  afterEach(function() {
    $('#calendar-add-appointment').dialog('close');
    $('#calendar-edit-appointment').dialog('close');
The dialog is no longer visible, but it is most definitely still in the DOM. Since it remains in the DOM, the guard clause in ensureDom() is met. The implication is that all future dialog operations take place on the empty Backbone <div>. Bah!

The solution, thankfully, is simple enough. In addition to closing the dialog between spec runs, I also need to remove the appointment dialog:
  afterEach(function() {
    $('#calendar-add-appointment').dialog('close');
    $('#calendar-add-appointment').remove();

    $('#calendar-edit-appointment').dialog('close');
  });
It will be recreated on the next run so any other tests that rely on its presence should continue to pass.

And indeed, my tests are passing again:


After applying the same technique to convert the edit-dialog to a in-Backbone operation, I call it a night. Up tomorrow, I hope to rid myself of fixtures altogether. They have proven to be a bit of a pain.


Day #207

Tuesday, November 15, 2011

Decoupling Page and Backbone.js

‹prev | My Chain | next›

Thanks to my spec fixin' efforts over the past few days, I now have a completely green jasmine test suite covering my Backbone.js calendar application. I think this is a good opportunity to refactor things a bit more.

The code behind the add-appointment dialog has been bugging me for a while. It works and I have tests covering it:


But the dialog itself is built and initialized outside of my Backbone app:
<div id="add-dialog" title="Add calendar appointment">
  <h2 class="startDate"></h2>
  <p>title</p>
  <p><input type="text" name="title" class="title"/></p>
  <p>description</p>
  <p><input type="text" name="description" class="description"/></p>
</div>

<script>
$(function() {
  $('#add-dialog').dialog({
    autoOpen: false,
    modal: true,
    buttons: [
      { text: "OK",
        class: "ok",
        click: function() { $(this).dialog("close"); } },
      { text: "Cancel",
        click: function() { $(this).dialog("close"); } } ]
  });
});
</script>
One of the oddities introduced by jQuery UI dialogs is that important dialog elements (e.g. form buttons) are added around the dialog() DOM element. In the above, a new dialog DOM element is added above <div id="add-dialog">, with the "add-dialog" then becoming a child of that dialog element.

I had to deal with that in my singleton view by setting the View's element to the parent() of "add-dialog":
    var AppointmentAdd = new (Backbone.View.extend({
      el: $("#add-dialog").parent(),
      // ....
    });
I would like to move the dialog HTML/DOM and the jQuery UI dialog() call all into my Backbone application. This will decouple my application from the web page hosting the application. This should also make it easier to test my Backbone app—I will no longer need to load in the entire fixture of the application's home page.

But, if I am to be successful in this endeavor, I will need to somehow account for the parent() weirdness that comes with jQuery UI dialogs.

First up, I delete the add-dialog HTML and Javascript from my web page. Then, in my View, I add an initialize method that will replace the stuff removed from the web page:
    var AppointmentAdd = new (Backbone.View.extend({
      initialize: function() {
        this.ensureDom();
        this.activateDialog();
      },
      // ....
    });
The ensureDom() method assumes the responsibility of the dialog HTML that used to be on my web page: ensuring that the HTML / DOM was present:
      ensureDom: function() {
        if ($('#calendar-add-appointment').length > 0) return;

        $(this.el).attr('id', 'calendar-add-appointment');
        $(this.el).attr('title', 'Add calendar appointment');

        $(this.el).append(this.make('h2', {'class': 'startDate'}));
        $(this.el).append(this.make('p', {}, 'Title'));
        $(this.el).append(this.make('p', {},
          this.make('input', {'type': "text", 'name': "title", 'class': "title"})
        ));

        $(this.el).append(this.make('p', {}, 'Description'));
        $(this.el).append(this.make('p', {},
          this.make('input', {'type': "text", 'name': "description", 'class': "description"})
        ));
      },
I am almost certainly going to remove all of that View#make stuff in there. Backbone Views expose a make() method for building very simple DOM elements. This dialog is too complicated for make() (and it is not truly complicate), but I needed an excuse to play with it.

At any rate, this ensureDom() method does nothing if it has already done its thing. "Its thing" begins by setting the View's ID attribute along with the "title" attribute (which is used by dialog() as the dialog title). The rest of the method simply converts the HTML to the make() equivalent (including a nested make() of the input fields).

Looking back on that, I can say that I now know how to use make(), but I probably will not make much use of it. It is far less readable than HTML strings.

Next up, I dialog-ify that DOM. Activating the dialog requires no more than copying the dialog() Javascript from my app page:
      activateDialog: function() {
        $(this.el).dialog({
          autoOpen: false,
          modal: true,
          buttons: [
            { text: "OK",
              class: "ok",
              click: function() { $(this).dialog("close"); } },
            { text: "Cancel",
              click: function() { $(this).dialog("close"); } } ]
        });
      }
Those two changes will build my dialog, but I have yet to account for the jQuery UI dialog parent() weirdness. In the initialize() method, I could try to say that the element is my newly initialized dialog parent:
    var AppointmentAdd = new (Backbone.View.extend({
      initialize: function() {
        this.ensureDom();
        this.activateDialog();
        this.el = $(this.el).parent();
      },
      // ...
    });
I could try, but my events would not work:
      events: {
        'click .ok':  'create',
        'keypress input[type=text]': 'createOnEnter'
      },
Backbone view events are initialized before initialize() is called. In this case, my 'click .ok' event would try to bind to an $('.ok') element in the DOM that I build, not the jQuery UI parent().

Happily, there is a easy remedy for this problem. I only need to manually invoke delegateEvents():
    var AppointmentAdd = new (Backbone.View.extend({
      initialize: function() {
        this.ensureDom();
        this.activateDialog();
        this.el = $(this.el).parent();
        this.delegateEvents();
      },
      // ...
    });
Backbone does this automatically—before it calls the initialize() method. The call to delegateEvents() also has the side-effect of removing any previously established handlers, so all is now as it should be.

And it works. When I click a date in my calendar, the add appointment dialog is shown:

That will suffice as a stopping point for tonight. Up tomorrow, I will work through the edit dialog and, hopefully, be able to eliminate the need for a fixture page in my jasmine tests entirely.


Day #206