Friday, September 16, 2011

Refactoring Backbone.js View Code

‹prev | My Chain | next›

Yesterday, I created a new Backbone.js view that was specialized for editing calendar appointments. I was not sure that this was going to be a better approach than building a non-Backbone interface. I am using jQuery UI's dialog for the editing interface, so it would not take much to code up a non-Backbone editing beasty.

But, in the end, access to the mode from the View object made it worth it:

    window.AppointmentEditView = Backbone.View.extend({
      // ...
      events : {
        'click .ok': 'update'
      },
      update: function() {
        this.model.save({
          title: $('.title').val(),
          description: $('.description').val()
        });
      }
    });
Click the OK button and the View's update() method is called. It can then call save() on the model taking update values from the form.

The model itself is assigned from the click event on the read-only Appointment View:
    window.AppointmentView = Backbone.View.extend({
      // ...
      handleEdit: function(e) {
        console.log("editClick");
        e.stopPropagation();

        var edit_view = new AppointmentEditView({model: this.model});
        edit_view.render();
      }
    });
I have no idea if that constitutes a best practice, but I rather like it. So today, I am going to convert my add-appointment View to do the same.

My day View had been manually opening the add-dialog:
    window.DayView = Backbone.View.extend({
      events : {
        'click': 'addClick'
      },
      addClick: function(e) {
        console.log("addClick");        

        $('#add-dialog').
          dialog('open').
          find('.startDate').
          html(this.el.id);
      }
    });
There was a separate Backbone view that was pre-attached to that dialog to create new appointments in the collection. Instead, I instruct the day View to create a new add-appointment View on click and render that view:
    window.DayView = Backbone.View.extend({
      events : {
        'click': 'addClick'
      },
      addClick: function(e) {
        console.log("addClick");        

        var add_view = new AppointmentAddView({model: this.model});
        add_view.render();
      }
    });
Next, I create the appointment-add View class, which looks suspiciously like my edit View class:
    window.AppointmentAddView = Backbone.View.extend({
      el: $("#add-dialog").parent(),
      render: function () {
        $('.ui-dialog-content', this.el).dialog('open');

        $('.startDate', this.el).html(this.startDate);
        $('.title', this.el).val("");
        $('.description', this.el).val("");
      },
      events: {
        'click .ok':  'create'
      },
      create: function() {
        var appointment = Appointments.create({
          title: this.el.find('input.title').val(),
          description: this.el.find('input.description').val(),
          startDate: this.el.find('.startDate').html()
        }
      }
    });
The element is the jQuery UI dialog (see yesterday for why I need to call parent()). Rendering requires that the dialog display and the values be reset for a new appointment. When the OK button is clicked, the create() method creates a new appointment in the Collection.

That is not quite the end the story, though. Thankfully, my Jasmine specs catch the mistakes that I have made. The first is that the add-dialog no longer has the date in it.


To resolve that, I need the day View to pass in the date In addition to the model:
    window.DayView = Backbone.View.extend({
      events : {
        'click': 'addClick'
      },
      addClick: function(e) {
        console.log("addClick");

        var add_view = new AppointmentAddView({
          model: this.model,
          startDate: this.el.id
        });
        add_view.render();
      }
    });
This requires that the appointment-add View squirrel that startDate away:
    window.AppointmentAddView = Backbone.View.extend({
      el: $("#add-dialog").parent(),
      initialize: function(options) {
        this.startDate = options.startDate;
      },
      // ...
    });
With that, I have my start date in the add dialog:
I still have one test failing, however. New appointments are no longer being added to the calendar UI:
The simplest way to resolve that is to take the return value from the collection's create() method and use that to instantiate a new appointment View:
    window.AppointmentAddView = Backbone.View.extend({
      // ...
      events: {
        'click .ok':  'create'
      },
      create: function() {
        var appointment = Appointments.create({
          title: this.el.find('input.title').val(),
          description: this.el.find('input.description').val(),
          startDate: this.el.find('.startDate').html()
        });

        var view = new AppointmentView({model: appointment});
        view.render();
      }
    });
With that, I have all of my jasmine specs passing again:
That will do for a stopping point today. Tomorrow, I hope to move the appointment View creation into an event rather than doing it inside the View's create() method.


Day #145

No comments:

Post a Comment