In a continuing effort to find my ideal namespacing scheme for my Backbone.js applications, I tried using a sprinkling of Javascript closures last night. The end result gave me the following overall structure:
var Cal = {};
Cal.Models = (function() {
var Appointment = Backbone.Model.extend({...});
return {Appointment: Appointment};
})();
Cal.Collections = (function() {
var Appointments = Backbone.Collection.extend({...});
return {Appointments: Appointments};
})();
Cal.Views = (function() {
var appointment_collection;
var Appointment = Backbone.View.extend({...});
var AppointmentEdit = Backbone.View.extend({...});
var AppointmentAdd = Backbone.View.extend({...});
var Day = Backbone.View.extend({...});
var Application = Backbone.View.extend({...});
return {Application: Application};
})();
var appointments = window.appointments = new Cal.Collections.Appointments,
app = new Cal.Views.Application({collection: appointments});
appointments.fetch();
I get two benefits from this approach. First, when Views spawn other Views (e.g. a click on the day View spawns an add-appointment View), I do not have to use the fully qualified view name. This leads to cleaner, more readable code: var Day = Backbone.View.extend({
addClick: function(e) {
var add_view = new AppointmentAdd({
model: this.model,
startDate: this.el.id
});
add_view.render();
}
});
The second benefit is that the most of the Views are private. Only the application View was returned / exported by the View function. The other Views in there are attached by the application View or each other, so there is no need for them to leak into the global namespace. Cleaner code, cleaner runtime.This approach still required me to use the full namespace (e.g.
Cal.Models.Appointment
) to describe Models inside Collections: var Appointments = Backbone.Collection.extend({
model: Cal.Models.Appointment,
// ...
});
I can live with that. As Nick pointed out in a comment the other day, this does not (or at least should not) happen much. What bothers me is how I have to build all of this outside of the
Cal
object literal: var appointments = new Cal.Collections.Appointments,
app = new Cal.Views.Application({collection: appointments});
appointments.fetch();
I do not think that the calling context should be responsible for knowing that the collection should be instantiated first. Nor should it know that the Collection needs to be passed to the application View. And finally the calling context should not be responsible for fetching the models from the server.Really, all I want to do is create a new "Calendar" application and have the rest taken care of by the object code. I think I can get that by making
Cal
a function constructor instead of the current object literal. Something along the lines of: var Cal = function() {...};
window.calendar = new Cal();
Inside the constructor function, I build up the Models
, Collections
, and Views
just like I did yesterday. I even keep the closures that let me treat much of it as private methods. By returning an object literal from the constructor function, I then create a constructor that can produce an identical API to the Cal
object literal from yesterday.The constructor function then becomes:
var Cal = function() {
var Models = (function() {
var Appointment = Backbone.Model.extend({...});
return {Appointment: Appointment};
})();
var Collections = (function() {
var Appointments = Backbone.Collection.extend({...});
return {Appointments: Appointments};
})();
var Views = (function() {
var Appointment = Backbone.View.extend({...});
var AppointmentEdit = Backbone.View.extend({...});
var AppointmentAdd = Backbone.View.extend({...});
var Day = Backbone.View.extend({...});
var Application = Backbone.View.extend({...});
return {Application: Application};
})();
// Externally accessible
return {
Models: Models,
Collections: Collections,
Views: Views
};
};
Now, if I wanted to, I could create a calendar object and access the appointment class like so:var cal = new Cal();
var appt = new cal.Models.Appointment();
So far, I do not have much different than yesterday's closure fest. To get some new functionality, I also add the initializer code to the constructor: var Cal = function() {
var Models = (function() {...})();
var Collections = (function() {...})();
var Views = (function() {...})();
// Initialize the app
var appointments = new Collections.Appointments;
new Views.Application({collection: appointments});
appointments.fetch();
return {
Models: Models,
Collections: Collections,
Views: Views,
appointments: appointments
};
};
With that, I can switch to the much simpler calendar constructor to start my Backbone app: window.calendar = new Cal();
I do have one spec failure:But it is a simple matter of updating the spec to use the new calendar application object:
it("displays model updates", function () {
var appointment = calendar.appointments.at(0);
appointment.save({title: "Changed"});
server.respondWith('PUT', '/appointments/42', '{"title":"Changed!!!"}');
server.respond();
expect($('#2011-09-15')).toHaveText(/Changed/);
});
With that, I am passing again:I certainly like the how the application can now be instantiated and run with a single constructor. I also like the closures borrowed from yesterday that give me private methods like the sub-Views. I worry that I will not remember why the closure ceremony was necessary if I return to the code in 6 months. There is not that much ceremony, but it could end up making the code less maintainable. I will ruminate on that.
Aside from that small concern, I do think this overall approach is pretty solid. Inside the constructor, I even get to reference things cross-concerns with one less namespace level — that is, I can create an instance of
Models.Appointment
instead of Cal.Models.Appointment
. Less typing make this a happy stopping point for the night. Well, less typing and a very small external interface.Day #140
I like the thinking behind this structure, but it really assumes one gigantic JS file . How would this look if you wanted to separate out your code into different files? (even if it all gets combined by a compiler pre-deploy)
ReplyDelete