After yesterday's Backbone.js session, my Recipes with Backbone co-author, Nick Gauthier, suggested a few improvements. I had gotten filtering working on my Backbone calendar so that only appointments from the currently displayed month would be fetched from the server. The setup code for this ran something like:
var appointments = new Collections.Appointments();
new Views.Application({collection: appointments});
var today = new Date(),
year = today.getFullYear(),
month = today.getMonth() + 1,
year_and_month = year + '-' + pad(month);
appointments.fetch({data: {date: year_and_month}});That code creates an instance of my appointments collections, hitches it to the application view, and then fetches the data from server. By invoking fetch() with the {data: {date: year_and_month}} argument, I am explicitly sending a query string of "date=2011-10" to the /appointments URL backing the collection.This can be problematic if any other part of the application forces an
appointments.fetch(). The collection instance no longer remembers the month, so the wrong month's appointments would be returned or worse—all of the appointments stored on the server could be returned.Storing the date in the model is a simple matter of adding an initialize method to the collection:
var Appointments = Backbone.Collection.extend({
model: Models.Appointment,
url: '/appointments',
initialize: function(options) {
options || (options = {});
this.date = options.date;
},
// ...
});Of course, simply storing the date in the collection has no benefit unless something else in the class can act upon it. Since I know that I need to fetch() the collections, I try overriding fetch(). This follows the familiar pattern of supporting the same argument as the real Backbone.Collection.prototype.fetch(). With that, I can call the method on the Backbone.Collection's prototype: var Appointments = Backbone.Collection.extend({
model: Models.Appointment,
url: '/appointments',
initialize: function(options) { /* ... */ },
fetch: function(options) {
options || (options = {});
var data = (options.data || {});
options.data = {date: this.date};
return Backbone.Collection.prototype.fetch.call(this, options);
},
// ...
});To make switching months slightly more convenient, I follow Nick's suggestion of adding a setDate() method on the collection that sets the new date and then calls fetch(): var Collections = (function() {
var Appointments = Backbone.Collection.extend({
// ...
setDate: function(date) {
this.date = date;
this.fetch();
},
// ...
});With that, I change the setup code to set the month when I instantiate the appointments collection instead of when I fetch it: var year_and_month = Helpers.iso8601(new Date()).substr(0,7),
appointments = new Collections.Appointments({date: year_and_month});
new Views.Application({collection: appointments});
appointments.fetch();Reloading the page, I find that everything is still working:Now, if I want to check out September's appointments, I only need redraw the calendar (with the non-Backbone
draw_calendar()) and make use of the new setDate() method on the collection:var september = "2011-09";
draw_calendar(september);
calendar.appointments.setDate('2011-09');Best of all, if I need to redraw for some reason, I can now fetch() the collection without having to remember the date:That works, but the overridden
fetch() method is a bit dense. Perhaps if I had followed my co-author's advice in the first place—by overriding url—I might be in a cleaner place now. Of course nothing is preventing me from trying that out now...I remove the overridden
fetch(). I also remove the url attribute, which had been set to '/appointments'. I replace the url attribute with a url() method (turns out Backbone collections can use either a url attribute or method): var Appointments = Backbone.Collection.extend({
// ...
url: function () {
return '/appointments/' + this.date;
},
// ...
});The collection in its entirety is now: var Appointments = Backbone.Collection.extend({
model: Models.Appointment,
initialize: function(options) {
options || (options = {});
this.date = options.date;
},
url: function () {
return '/appointments/' + this.date;
},
setDate: function(date) {
this.date = date;
this.fetch();
},
parse: function(response) {
return _(response.rows).map(function(row) { return row.value ;});
}
});That url() method is much cleaner than the fetch() that I had earlier. There is a cost, however. The server now needs to respond to a URL like '/appointments/2011-10' as if this were requesting a list of records rather than a single record with ID of '2011-10'. In other words, I am no longer RESTful.In this case, I am not using the GET single record resource, so I go ahead and make the change in my express.js server:
app.get('/appointments/:date', function(req, res){
var options = {
host: 'localhost',
port: 5984,
path: '/calendar/_design/appointments/_view/by_month?key="'+ req.params.date +'"'
};
http.get(options, function(couch_response) { /* ... */ });
});It might be better to create a new collection resource like /month_appointments to support this. And I still might do that in the future. But, for now I live with my non-RESTful '/appointments/2011-10' route.Trying this out in the Javascript console, I see that it still works:
And the correct appointments—all from September—are now displayed in my Backbone app:
So, in the end, overriding
url() makes for some clean code. It does potentially violate REST, but there ought to be workarounds (i.e. with collection resources). I can still see some benefit to overriding fetch() to accomplish the same thing. Although denser, it does offer better ability to work with existing resources—something that can be especially handy when making backend changes is non-trivial.Up tomorrow: switching months without the Javascript console (this time I mean it).
Day #168





0 comments:
Post a Comment