Tonight, I continue to refactor the calendar view in my Backbone.js application:
The appointments on the calendar are small Backbone views. The calendar itself is a recent convert to a Backbone view, the template of which is:
<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>After the Calendar view renders, I have a fixer-upper method work with that empty calendar to add date IDs to the
<td>
cells and to draw the day of the month number inside the cells: 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
}));
this.addDates();
this.hideEmptyWeeks();
return this;
},
addDates: function() {
var firstOfTheMonth = Helpers.firstOfTheMonth(this.date),
month = firstOfTheMonth.getMonth(),
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 = Helpers.dayAfter(date);
});
});
},
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()
}
}));
Yikes! One thing I definitely notice (aside from tons to code) it that I repeat the iteration of the weeks both in the tempate and in the fixer-upper method. What this tells me is that I either need to move all of the calculation into the template or into Backbone views. Somehow doing this in templates feels wrong, so I give Backbone views a try.First, I eliminate the template and all of that fixer-upper code. My calendar's
render()
then becomes nothing more than a call to two other views: a calendar header view (the days of the week at the top of the calendar) and the calendar body: var Calendar = new (Backbone.View.extend({
el: $('#calendar'),
setDate: function(date) {
this.date = date;
this.render();
},
render: function() {
$('span.year-and-month', 'h1').html(' (' + this.date + ')');
$(this.el).html('<table></table>');
var table = $('table', this.el);
var header = new CalendarMonthHeader({parent: table})
header.render();
var body = new CalendarMonthBody({
parent: table,
date: this.date
});
body.render();
return this;
}
}));
That is considerably less code than my template + fixer-upper. But how much work is needed in the various sub-views?The calendar header is blissfully simple:
var CalendarMonthHeader = Backbone.View.extend({
tagName: 'tr',
initialize: function(options) {
this.parent = options.parent;
},
render: function() {
$(this.el).html('<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>');
this.parent.append(this.el);
}
});
Simple, but then again, there are no calculations required. What about the rest of the calendar?The body view class delegates all rendering to sub-views. Specifically, it iterates over weeks, creating new calendar month week views:
var CalendarMonthBody = Backbone.View.extend({
initialize: function(options) {
this.parent = options.parent;
this.date = options.date;
},
render: function() {
var firstOfTheMonth = Helpers.firstOfTheMonth(this.date),
month = firstOfTheMonth.getMonth(),
firstSunday = new Date(firstOfTheMonth.getTime() -
firstOfTheMonth.getDay()*24*60*60*1000);
var date = firstSunday;
while (date == firstSunday || date.getMonth() <= month) {
var week = new CalendarMonthWeek({
parent: this.parent,
date: date
});
week.render();
date = Helpers.weekAfter(date);
}
}
});
Most of the effort in that code is date calculations, which are a pain in Javascript. The week view does nothing more than create a table row to which day views are attached:
var CalendarMonthWeek = Backbone.View.extend({
tagName: 'tr',
initialize: function(options) {
this.parent = options.parent;
this.date = options.date;
},
render: function() {
this.parent.append(this.el);
var date = this.date;
for (var i=0; i<7; i++) {
var day = new CalendarMonthDay({
parent: $(this.el),
date: date
});
day.render();
date = Helpers.dayAfter(date);
}
}
});
And finally, the day view build table cells with the ISO 8601 date for the ID (used by the appointment views to attached to the calendar) and a <span>
tag containing the day of the month: var CalendarMonthDay = Backbone.View.extend({
tagName: 'td',
initialize: function(options) {
this.parent = options.parent;
this.date = options.date;
},
render: function() {
this.parent.append(this.el);
this.el.id = Helpers.to_iso8601(this.date);
var html = '<span class="day-of-month">' + this.date.getDate() + '</span>';
$(this.el).html(html);
return this;
}
});
Phew! Amazingly, that works:I am none too certain that this is all that superior a solution to the template. The view code in the template was not too overwhelming. Still, each of these views is small and self-contained. Small and self-contained makes for easier testing and maintainability.
Day #184
No comments:
Post a Comment