Tuesday, September 20, 2011

Javascript Closures for Namespacing Backbone.js Apps

‹prev | My Chain | next›

Up tonight, I continue exploring namespacing my Backbone.js calendar application.

Last night, I tried manually building up a Cal namespace:
var Cal = {
      "Models": {},
      "Collections": {},
      "Views": {}
    }
This worked, but had at least one annoyance—the necessity of referring to object in the global namespace (e.g. Cal.Models.Appointment) throughout the application. In certain parts this seems unavoidable. For instance, Backbone collections need to reference a model, so I need to specify the fully namespaced model:
    Cal.Collections.AppointmentList = Backbone.Collection.extend({
      model: Cal.Models.Appointment,
      // ...
    });
I could probably live with that, but it just gets annoying in the various views. The day View needs to open the add-appointment View when clicked, necessitating a global namespace:
    Cal.Views.DayView = Backbone.View.extend({
      // ...
      addClick: function(e) {
        console.log("addClick");

        var add_view = new Cal.Views.AppointmentAddView({
          model: this.model,
          startDate: this.el.id
        });
        add_view.render();
      }
    });
The appointment View needs to open the edit-appointment View when clicked—global namespace. Collection events (e.g. 'add', 'reset') need to bind to view actions—global namespace. It winds up being noisy, which decreases the overall readability of the code.

It would require a significantly different approach to avoid global namespaces across concerns (i.e. accessing the model classes from collections). It does not take much effort to avoid that within a single concern. For instance, I can change the View class namespace from:
    Cal.Views.AppointmentView = Backbone.View.extend({...});
    Cal.Views.AppointmentEditView = Backbone.View.extend({...});
    Cal.Views.AppointmentAddView = Backbone.View.extend({...});
    Cal.Views.DayView = Backbone.View.extend({...});
    Cal.Views.AppView = Backbone.View.extend({...});
Instead I can put all of the views inside a single anonymous function that returns an object literal of those views. Setting Cal.Views to the result of that function winds up building the same namespace:
    Cal.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 {
        Appointment: Appointment,
        AppointmentEdit: AppointmentEdit,
        AppointmentAdd: AppointmentAdd,
        Day: Day,
        Application: Application
      };
    })();
Now, when the Day View class needs to open an AppointmentAdd View, it can call just AppointmentAdd instead of Cal.Views.AppointmentAdd:
      var Day = Backbone.View.extend({
        addClick: function(e) {
          console.log("addClick");

          var add_view = new AppointmentAdd({ /* ... */ });
          add_view.render();
        }
      });
Following along that convention, I can encapsulate my entire Backbone application as:
    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};
    })();
I was even able to eliminate much of what the Cal.Views namespace needed because the Application view is responsible for attaching the remainder. Thus, to start the Backbone app, I can instantiate the collection object, pass it to the application view, and fetch appointments from the backend:
   var appointments = new Cal.Collections.Appointments,
        app = new Cal.Views.Application({collection: appointments});

    appointments.fetch();
I rather like that. By using simple Javascript closures, I achieve significant improvement in readability inside the three Backbone namespaces. As a bonus, I have very limited external API required—just enough to build and populate the application collection and to ensure that the various views have access to that collection.

My jasmine tests, however, would like to have a word:
This turns out to be a reference to the collection instance rather than to a class that I have been refactoring. It seems that the "application" View is not the only one that needs access to this collection. The appointment-add View needs to be able to use this collection to create new appointments:
          var AppointmentAdd = Backbone.View.extend({
        // ...
        create: function() {
          Appointments.create({
            title: this.el.find('input.title').val(),
            description: this.el.find('input.description').val(),
            startDate: this.el.find('.startDate').html()
          });
        }
      });
Ugh. I see no way around this, but to add an appointment_collection variable to the Cal.Views closure:
    Cal.Views = (function() {
      var appointment_collection;

      // ...
    });
I can then set that variable when the application View object is initialized, making it available for when the appointment-add View needs it:

    Cal.Views = (function() {
      var appointment_collection;

      var Application = Backbone.View.extend({
        initialize: function(options) {
          this.collection = appointment_collection = options.collection;

          this.initialize_appointment_views();
          this.initialize_day_views();
        },
        // ...
      });

      var AppointmentAdd = Backbone.View.extend({
        create: function() {
          appointment_collection.create({
            title: this.el.find('input.title').val(),
            description: this.el.find('input.description').val(),
            startDate: this.el.find('.startDate').html()
          });
        }
      });

      // ...
      return { Application: Application };
    })();
That is all kinds of ugly, but it has me in the green again for all of my tests:
I suppose this is why many example Backbone applications have the object instances in the same namespace as the classes. I am not overly fond of the idea, but I can seen how it happens. I will continue exploring this tomorrow. I will most likely take the advice of my co-author on Recipes with Backbone by exploring using call() to pass around at least some of this information.

Day #139

2 comments:

  1. Great, informative post!

    Quick question: isn't render(), at its core, a closure? i.e., once 'return this;' is applied to the method, isn't in effect performing a closure's key functionality?

    ReplyDelete
    Replies
    1. Any function in JavaScript can be a closure -- it just needs a reference to a value defined outside of it. In the case of the typical render(), I wouldn't consider it a closure. The `this` value refers to the object itself (i.e. the function's current context). I suppose that you could consider `this` as being defined outside the function, but I don't typically think of it that way. Then again, maybe that's just me viewing JavaScript through too much of a classical OOP lens :)

      Delete