After refactoring the views in my appointment calendar Backbone.js application, I am ready to move onto to other topics. Except that I forgot a TODO that I left myself in the month View:
var CalendarMonth = Backbone.View.extend({
tagName: 'table',
initialize: function(options) {
this.date = options.date;
},
render: function() {
// TODO:
// $('span.year-and-month', 'h1').html(' (' + this.date + ')');
// ..
}
});
Ah. My old template-based view was doing so much that I had not felt bad about adding a side-effect in there as well. This particular side-effect adds the date to the page's <h1>
tag.I want a Backbone view that is dedicated to updating the page's title—a title view. If the page title has a
year-and-month
span in it:<h1> Funky Calendar <span class="year-and-month"></span> </h1>Then I want my title view to populate said
<span>
tag whenever the month changes. The appointment collection only holds appointments from the current month. It also exposes a
getDate()
getter for the current month. I should be able to use this to update the title: var TitleView = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
$('span.year-and-month', 'h1').
replaceWith(this.el);
},
render: function() {
$(this.el).html(' (' + this.collection.getDate() + ') ');
}
});
My main Application view is injected with the month appointments collection:appointments = new Collections.Appointments({date: year_and_month}), application = new Views.Application({ collection: appointments, el: root_el });It can then inject the same collection into the title view when it creates it:
var Application = Backbone.View.extend({
initialize: function(options) {
// ...
this.initialize_title();
},
// ...
initialize_title: function() {
new TitleView({collection: this.collection});
},
// ...
});
But how to get that to change when the user navigates between months? When it was a side-effect of the calendar render()
, that was easy. But now?The answer is to use events—this is Backbone after all. In the appointment collection, I trigger a custom event,
calendar:change:date
, after successfully fetching a month's appointments: var Appointments = Backbone.Collection.extend({
model: Models.Appointment,
// ...
fetch: function(options) {
options || (options = {});
var data = (options.data || {});
options.data = {date: this.date};
var collection = this;
var success = options.success;
options.success = function (resp, status, xhr) {
collection.trigger('calendar:change:date');
if (success) success(collection, resp);
};
return Backbone.Collection.prototype.fetch.call(this, options);
},
// ...
});
By triggering the event in the success callback of my fetch()
class, I ensure that the date in the calendar's title will only change if the next month is successfully obtained. The dance of saving existing
success
callbacks might be unnecessary future-proofing on my part. I have gotten in the habit of doing that, especially in my models where is makes more sense. I am not currently calling this collection's fetch()
with any options, so ensuring that a success callback is retained is definitely overkill. If I ever did want to pass in a success callback, I would likely check to ensure that fetch()
was not overridden and, if it was, that it supported the success
callback. Still, this feels like a good habit to have.The event itself,
calendar:change:date
is chosen to avoid conflicting with "real" events. If the model's date is updated in the application, the model automatically emits a change:date
event. To prevent confusion with that "real" event, I opt for a clearer namespaced event. There is no need to update the title of the page if the user updates an appointment! With my collection successfully generating the event, I can now bind to that event in my view:
var TitleView = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
options.collection.bind('calendar:change:date', this.render, this);
$('span.year-and-month', 'h1').
replaceWith(this.el);
},
render: function() {
$(this.el).html(' (' + this.collection.getDate() + ') ');
}
});
With that, I have dates back in the title of funky calendar:For good measure, I also tie my calendar navigation view to listen for this event (so that next and previous links can use the proper months).
I really felt bad about updating the title as a side-effect of rendering the new calendar month. This is a bit more code, but it feels much cleaner—mainly because it only has a single responsibility. In addition to clearer intent (what could be clearer than "TitleView"?), I am secure knowing that, should I need to make this more sophisticated in the future, I know where changes need to go.
Day #189
No comments:
Post a Comment