Sunday, October 23, 2011

Replacing Legacy Views with Backbone.js

‹prev | My Chain | next›

Yesterday, I got started looking at Underscore.js templating for my Backbone.js calendar application. It was trivial to replace the default ERB style <%= ... %> variable interpolation with mustache style {{ ... }} structures.

I also replaced <% ... %> code evaluation structures with the un-mustache like {[ ... ]}. I hope to use that to replace the legacy calendar "view" with which I have been stuck. The appointments, add/update forms, and month navigation controls all come from Backbone code in my app:

The calendar itself comes from a pre-Backbone static HTML:
<h1>Funky Calendar<span class="year-and-month"></span></h1>
<table id="calendar">
<thead>
  <tr>
    <th>S</th>
    <th>M</th>
    <th>T</th>
    <th>W</th>
    <th>T</th>
    <th>F</th>
    <th>S</th>
  </tr>
</thead>

<tr id="week0">
  <td class="sunday"></td>
  <td class="monday"></td>
  <td class="tuesday"></td>
  <td class="wednesday"></td>
  <td class="thursday"></td>
  <td class="friday"></td>
  <td class="saturday"></td>
</tr>
<!-- .... -->
<tr id="week5">
  <td class="sunday"></td>
  <td class="monday"></td>
  <td class="tuesday"></td>
  <td class="wednesday"></td>
  <td class="thursday"></td>
  <td class="friday"></td>
  <td class="saturday"></td>
</tr>
</table>
I probably need to keep the header row containing the days of the week, but the 6 table rows containing the actual calendar days can get DRYed up. Last night, I converted this to an underscore template (making use of the un-mustache {[ ... ]} construct):
<script type="text/template" id="calendar-template">
<table>
  <thead>
    <tr>
      <th>S</th>
      <th>M</th>
      <th>T</th>
      <th>W</th>
      <th>T</th>
      <th>F</th>
      <th>S</th>
    </tr>
  </thead>
  <tbody>
  {[ _([0,1,2,3,4,5]).each(function(i) { ]}
  <tr class="week{{ i }}">
    <td class="sunday"></td>
    <td class="monday"></td>
    <td class="tuesday"></td>
    <td class="wednesday"></td>
    <td class="thursday"></td>
    <td class="friday"></td>
    <td class="saturday"></td>
  </tr>
  {[ }); ]}
  </tbody>
</table>
</script>
Tonight I would like to create a Backbone view to make use of this template.

So, I replace all of that static HTML with:
<h1>
  Funky Calendar
  <span class="year-and-month"></span>
</h1>

<div id="calendar"></div>
To populate the now empty <div id="calendar"> tag, I start with the Router which now needs to tell the calendar view to render itself for the correct date:
      var Routes = Backbone.Router.extend({
        routes: {
          // ...
          "month/:date": "setMonth"
        },
        // ...
        setMonth: function(date) {
          console.log("[setMonth] %s", date);
          Views.Calendar.setDate(date);
          appointments.setDate(date);
        }
      });
Here I am going to call the Views.Calendar as a singleton view (one of the recipes from Recipes with Backbone). I define that view simply as:
        var Calendar = new (Backbone.View.extend({
          el: $('#calendar'),
          template: _.template($('#calendar-template').html()),
          setDate: function(date) {
            this.date = date;
            this.render();
          },
          render: function() {
            $('span.year-and-month', 'h1').html(this.date);

            $(this.el).html(this.template({
              date: this.date
            }));

            return this;
          }
        }));
The setDate() method is a simple wrapper to set the date instance variable and to tell the view to render itself. I explicitly tell the view to attached itself to the '#calendar' element. At some point I should make that dynamic so that it can configured, but that will do for now. With that, I have my empty calendar:


To get the dates added to the calendar, as well as to assign the ISO 8601 date as the ID for each of the cells in the calendar, I pull in some of the old app code:
        var Calendar = new (Backbone.View.extend({
          // ...
          render: function() {
            // ...
            $(this.el).html(this.template({ date: this.date }));

            this.addDates();
            this.hideEmptyWeeks();

            return this;
          },
          addDates: function() {
            var year = parseInt(this.date.split(/\\D/)[0], 10),
                month = parseInt(this.date.split(/\\D/)[1], 10) - 1,
                firstOfTheMonth = new Date(year, month, 1),
                firstSunday = new Date(firstOfTheMonth.getTime() -
                                firstOfTheMonth.getDay()*24*60*60*1000);

            var date = firstSunday;
            _([0,1,2,3,4,5]).each(function(week) {
              var week_el = $('.week' + week, this.el),
                  day_elements = week_el.find('td');

              _([0,1,2,3,4,5,6]).each(function(day) {
                var day_element = day_elements[day],
                    other_month = (date.getMonth() != month),
                    html = '<span class="day-of-month">' + date.getDate() + '</span>';

                $(day_element).
                  html(html).
                  attr('id', Helpers.to_iso8601(date)).
                  addClass(other_month ? 'other-month' : '');

                date = new Date(date.getTime() + 24*60*60*1000);
              });
            });
          },
          hideEmptyWeeks: function() {
            // remove whole week from next month
            var week5_other_month = _($('td', '.week5')).all(function(el) {
              return $(el).hasClass('other-month')
            });
            if (week5_other_month) { $('.week5').hide() }
          }
        }));
Now, that is some ugly, non Backbone-y code, but it works:
I am torn between trying to do all of that inside a template or leave it more or less as is. That seems like it could get ugly real quick in a template. After all, it is already pretty ugly.

I will sleep on that and pick back up here tomorrow.


Day #183

2 comments:

  1. Hi Chris, found this after googling for something similar and wondered if you had any followup on it?
    I agree that it is some ugly non backboney code and I am also trying to do something similar but in ember. Did this get resolved or shelved?

    ReplyDelete
    Replies
    1. The next night, I dropped all of this for a series of sub-views (CalendarMonth, CalendarMonthBody, CalendarMonthWeek, and CalendarMonthDay). The post is at: http://japhr.blogspot.com/2011/10/refactoring-large-backbonejs-views-into.html.

      That ultimately formed what is now my final solution at: https://github.com/eee-c/Funky-Backbone.js-Calendar/tree/master/public/scripts/Calendar (that also relies on requirejs).

      Hope that helps!

      Delete