Showing posts with label jquery-ui. Show all posts
Showing posts with label jquery-ui. Show all posts

Tuesday, October 4, 2011

Backbone.js Validations and jQuery UI

‹prev | My Chain | next›

I got started with Backbone.js validations yesterday. My initial approach was, at best, rudimentary. Fortunately, my co-author on the forthcoming Recipes with Backbone, Nick Gauthier, had some advice on how to improve.

So first up, I drop my basic validation handler in favor of one on the my view. Each appointment in my calendar application is controlled by a Backbone view, so this is where I add my error handling:
        var Appointment = Backbone.View.extend({
          template: _.template($('#calendar-appointment-template').html()),
          initialize: function(options) {
            // ...
            options.model.bind('error', this.handleError, this);
          },
          handleError: function(model, error) {
            if (error.status == 409) {
              alert("This site does not understand CouchDB revisions.");
            }
            else if (typeof(error == 'Array')) {
              alert(error.join("\\n"));
            }
            else {
              alert("This site was made by an idiot.");
            }
          },
        });
The 409 HTTP status check is for errors that might originate from my CouchDB data store. As Nick points out, I should move this into Backbone.sync so that my view does not have to be aware of things like the data store's error messages. I will worry about that another day.

For now, this addition of the check for errors of type 'Array', should account for the errors that I am setting in my model:
      var Models = (function() {
        var Appointment = Backbone.Model.extend({
          // ...
          validate: function(attributes) {
            var errors = [];

            if (!(/\\S/.test(attributes.title)))
              errors.push("Title cannot be blank.");

            if (!/\\S/.test(attributes.description))
              errors.push("Description cannot be blank.");

            if (errors.length > 0)
              return errors;
          },
          // ...
        });
And this does work. When I attempt to edit an existing appointment and remove the required description, I am greeted with:
That is fine and dandy, but I get no error feedback at all when creating appointments. My add-appointment view is a singleton view, primarily because it is using a jQuery UI dialog to present the form:
Nick had suggested binding a view's event listener to the model's error handler, similar to what I am doing with the appointment view:
        var Appointment = Backbone.View.extend({
          template: _.template($('#calendar-appointment-template').html()),
          initialize: function(options) {
            // ...
            options.model.bind('error', this.handleError, this);
          },
          // ...
        });
But I have to confess that I do not know how I would go about doing this with a singleton view. What I do know how to do in a singleton view is add an "error" callback to the create() method:
var AppointmentAdd = new (Backbone.View.extend({
          // ...
          events: {
            'click .ok':  'create'
          },
          create: function() {
            var attributes = {
              title: this.el.find('input.title').val(),
              description: this.el.find('input.description').val(),
              startDate: this.el.find('.startDate').html()
            };

            var options = {
              success: function() { $('#add-dialog').dialog("close"); },
              error: function(model, errors) {
                $('.errors', '#add-dialog').html(errors.join("<br/>")).show();
              }
            };

            appointment_collection.create(attributes, options);
          }
        }));
Now, when I try to create an invalid appointment, I am greeted with:
This gives me a chance to correct my mistake and save successful. Upon successful save, the dialog is hidden from view.

That kinda works. I will call it a night here and see what my co-author (or anyone else) has to say about my solution.

Day #153

Sunday, September 25, 2011

Duplicate Backbone Events with jQuery UI Dialogs

‹prev | My Chain | next›

Up tonight, I hope to continue making progress switching entirely to faye for my Backbone.js application's persistence layer. I can populate appointments in the calendar via Faye, so up tonight I hope to be able to add and update calendar appointments.

To figure out where to start, I open the add-dialog and press OK:
Since the persistence layer has already been swapped out by replacing Backbone.sync:
    var faye = new Faye.Client('/faye');
    Backbone.sync = function(method, model, options) {
      faye.publish("/calendars/" + method, model);
    }
And, since I am logging the various CRUD operations:
    _(['create', 'update', 'delete', 'read', 'changes']).each(function(method) {
      faye.subscribe('/calendars/' + method, function(message) {
        console.log('[/calendars/' + method + ']');
        console.log(message);
      });
    });
Then the create operation shows up in the Javascript console:
Thus, I know that the create operation is being published to Faye, so I need to handle it on the backend. For that, I adopt my server side faye listener from last night. This time, I subscribe to the /calendars/create channel. When a message is received there, I POST into the CouchDB backend store, accumulate the CouchDB response and, when ready, send back the actual Javascript object:
client.subscribe('/calendars/create', function(message) {
  // HTTP request options
  var options = {
    method: 'POST',
    host: 'localhost',
    port: 5984,
    path: '/calendar',
    headers: {'content-type': 'application/json'}
  };

  // The request object
  var req = http.request(options, function(response) {
    console.log("Got response: %s %s:%d%s", response.statusCode, options.host, options.port, options.path);

    // Accumulate the response and publish when done
    var data = '';
    response.on('data', function(chunk) { data += chunk; });
    response.on('end', function() {
      client.publish('/calendars/add', JSON.parse(data));
    });
  });

  // Rudimentary connection error handling
  req.on('error', function(e) {
    console.log("Got error: " + e.message);
  });

  // Write the POST body and send the request
  req.write(JSON.stringify(message));
  req.end();
});
And that seems to work. I get my HTTP 201 response back from CouchDB (man, I love me a HTTP DB) and the newly created object is broadcast back on the /calendars/add faye channel. Even the browser sees it. But something weird happens when I create a second appointment—it gets created twice. And something weird happens when I create a third appointment—it is created three times. I see this in the console.log() output in Chrome's Javascript console and I see it in the server's logs:
{"title":"#1","description":"asdf","startDate":"2011-09-03"}
Got response: 201 localhost:5984/calendar
{"title":"#2","description":"asdf","startDate":"2011-09-03"}
Got response: 201 localhost:5984/calendar
{"title":"#2","description":"asdf","startDate":"2011-09-03"}
Got response: 201 localhost:5984/calendar
{"title":"#3","description":"asdf","startDate":"2011-09-03"}
{"title":"#3","description":"asdf","startDate":"2011-09-03"}
{"title":"#3","description":"asdf","startDate":"2011-09-03"}
Got response: 201 localhost:5984/calendar
Got response: 201 localhost:5984/calendar
Got response: 201 localhost:5984/calendar
So I add a debugger statement to the add-appointment View's click handler:
        var AppointmentAdd = Backbone.View.extend({
          // ...
          events: {
            'click .ok':  'create'
          },
          create: function() {
            debugger;
            appointment_collection.create({
              title: this.el.find('input.title').val(),
              description: this.el.find('input.description').val(),
              startDate: this.el.find('.startDate').html()
            });
          }
        });
The first time I hit that debugger statement, I allow code execution to continue. And then something strange happens... I hit that same event handler again:
Dammit. Every time I open that jQuery UI dialog for adding appointments, I am adding another copy of the same event handler to it. This is not being caused by my switch to faye—this is something that I had not noticed until inadvertent testing while mucking with faye.

Backbone actually has a strategy for preventing this kind of thing from happening. Unfortunately for me, that strategy expects that the element in question is created anew each time the view is initialized. In this case, I re-show() a dialog that was previously hidden.

Backbone's strategy involves assigning a unique ID to the view instance. Before assigning new events to the View's element, Backbone first removes any existing event handlers bound with that unique ID. I wonder if I can use this to my advantage. Perhaps when I initialize the view, I can always assign the same unique ID:
        var AppointmentAdd = Backbone.View.extend({
          initialize: function(options) {
            this.startDate = options.startDate;
            this.cid = 'add-dialog';
          },
          // ...
        });
Sadly, that does not work. I am still getting multiple appointments added on second and third add dialogs. Rooting through the Backbone code, I find that this is because the delegateEvents() method is called before initialize():
  Backbone.View = function(options) {
    this.cid = _.uniqueId('view');
    this._configure(options || {});
    this._ensureElement();
    this.delegateEvents();
    this.initialize.apply(this, arguments);
  };
Thus, by the time I assign my constant cid, it is already too late—events have been bound to the non-worky cid. I am hesitant to hook into any of those other methods—the underscore at the front of the method names indicate that these ought to be treated as private methods. Sure, I could do it, but I would feel ashamed at reaching under the covers like that.

This leaves the delegateEvents() method, which is not only a public method, but also one documented in the API. Surely that is fair game. So I override it, setting the constant cid and then call() the original version of delegateEvents() from the Backbone.View's prototype:
        var AppointmentAdd = Backbone.View.extend({
          // ...
          delegateEvents: function(events) {
            this.cid = 'add-dialog';
            Backbone.View.prototype.delegateEvents.call(this, events);
          },
          // ...
        });
And that works! Now, when I add a second or third appointment to my calendar, it only adds a single event:
{"title":"#1","description":"asdf","startDate":"2011-09-05"}
Got response: 201 localhost:5984/calendar
{"title":"#2","description":"asdf","startDate":"2011-09-05"}
Got response: 201 localhost:5984/calendar
I call it a night there. I still need to finish the add circle to ensure that new appointments are added to the existing collection (and to the UI). Then I really need to get delete working. I have a lot of appointments cluttering up my calendar now...


Day #144

Friday, September 2, 2011

Interactively Adding Things with jQuery UI and Backbone.js

‹prev | My Chain | next›

Yesterday I was able to get a jQuery UI dialog updating a Backbone.js model. Still outstanding is being able to initiate the dialog (by means other than the Javascript console) and real-time update of my calendar UI.

I think the first thing to try is making a "Day" Backbone View. This view will be responsible for handling clicks to add new events. At some point, this responsibility should probably move down into an "Add Appointment" View or something more specific to adding appointments. For now, I am satisfied to be able to click on a day and open the jQuery UI dialog with the appropriate date chosen. This ought to be a good start:

  $(function() {
    // ...
    window.DayView = Backbone.View.extend({
      events : {
        'click': 'click'
      },
      click: function() {
        console.log(this.el);
      }
    });
  });
Obviously the click() method will have to do more than log the element being clicked, but this is just a tracer bullet for the target that I ultimately hope to hit. To use that View class, I need to instantiate a Day View object for every day on my funky, funky calendar:
Adding the following ought to do the trick:
    $('#calendar td').each(function() {
      new DayView({el: this});
    });
Nothing too fancy there, for every <td> table cell, create a new DayView with that table cell as the reference element. Specifying a reference element ensures not only that Backbone will associate DOM events with the correct HTML element, but also that Backbone will not try to build its own elements to be inserted into the document later.

After reloading the page and clicking the first, second and third days of this month, the Javascript console sees the correct cells receiving the click event:
Safe in the knowledge that my tracer bullets are hitting where I hope, I am ready to replace the console.log() with a jQuery UI dialog-open:

    window.DayView = Backbone.View.extend({
      events : {
        'click': 'click'
      },
      click: function() {
        $('#dialog').
          dialog('open').
          find('.startDate').
          html(this.el.id);
      }
    });
In addition to opening the dialog, I also find the .startDate element and replace its contents with the ID of the clicked cell. Since my calendar cell IDs are ISO 8601 dates, the result is that the dialog has the date set:
If I close that dialog and click another date cell, the previous date (inside the .startDate element) is replaced with the new ID/date. Easy-peasy. The dialog always shows the date for the calendar cell clicked because the ID for all of the calendar cells is the ISO 8601 formatted date for that cell.

My jQuery dialog View is already able to create the calendar appointment (a.k.a event) by reading the form field values. I can get the start date from the .startDate's HTML:

    window.AppView = Backbone.View.extend({
      // ...
      create: function() {
        Events.create({
          title: this.el.find('input.title').val(),
          description: this.el.find('input.description').val(),
          startDate: this.el.find('.startDate').html()});
      }
    });
And it works! I can create lots of appointments now:

The only thing is that I have to reload the page to see those events show up on the calendar. Surely it is possible to do better in Backbone...?

When the page first loads, it runs through each element in the Events collection. For each pre-existing appointment/event, an EventView object is created and rendered:

    Events.fetch({success: function(collection, response) {
        collection.each(function(event) {
          var el = $('#' + event.get("startDate")),
              view = new EventView({model: event, el: el});
          view.render();
        });
      }
    });
Might something similar work for newly created events? Indeed it does.

The Events.create() method actually returns a Backbone model—just like the on used to create a new EventView on page load. Once I have that, I can create a new EventView object and render it immediately:

    window.AppView = Backbone.View.extend({
      // ...
      create: function() {
        var event = Events.create({
          title: this.el.find('input.title').val(),
          description: this.el.find('input.description').val(),
          startDate: this.el.find('.startDate').html()
        });

        var el = $('#' + event.get("startDate")),
            view = new EventView({model: event, el: el});
        view.render();
      }
    });
Nice! Now I can create and destroy calendar events / appointments with impunity and have them persist on page reload.

I still have a ton of cleanup to do. Renaming a few things is definitely in order. A test or two couldn't hurt. Also, clicking on the delete-event icon also results in a click on the day itself. But all-in-all, I am rather pleased with how this came together.


Day #131

Thursday, September 1, 2011

jQuery UI and Backbone.js

‹prev | My Chain | next›

Before doing anything else with my little Backbone.js calendar application, I would like to be able to add new appointments / calendar events. I have been doing that via the CouchDB backend and it is getting a bit old. Besides, with the new month, most of my events have disappeared:
But how to add these appointments?

I rather fancy a jQuery-ui modal dialog box that pops up when I click on the appropriate day. But, I have no idea where to hook the jQuery-ui dialog into my Backbone app...

First things first, I download and install jQuery-ui (the javascript and the theme css) and add it to my Jade layout template:
!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    link(rel='stylesheet', href='/stylesheets/blitzer/jquery-ui.css')
    script(src='/javascripts/jquery.min.js')
    script(src='/javascripts/jquery-ui.min.js')
    script(src='/javascripts/underscore.js')
    script(src='/javascripts/backbone.js')
  body!= body
Next, I create a very simple dialog in the Jade template:
#dialog(title="Add calendar event")
  #calendar-event-start-date
  p title
  p
    input#calendar-event-title(type="text", name="title")
  p description
  p
    input#calendar-event-description(type="text", name="description")
I have the intention of eventually grabbing the values for appointments from the two dialog fields and from the #calendar-event-start-date <div> (which I will populate from the date clicked). But before I reach that point, I need to make this a jQuery-ui dialog:
script
  $(function() {
    $('#dialog').dialog({
      autoOpen: false,
      modal: true,
      buttons: [
        { text: "OK",
          click: function() { $(this).dialog("close"); } },
        { text: "Cancel",
          click: function() { $(this).dialog("close"); } }
      ]
    });
  });
So far, there is absolutely nothing Backbone-y about this. I change that by adding an AppView Backbone View class:

    window.AppView = Backbone.View.extend({
      el: $("#dialog"),
      events: {
        'click .ok':  'create'
      },
      create: function() {
        console.log("here");
        Events.create({
          title: "foo",
          description: "bar",
          startDate: "2011-09-01"});
      }
    });

    window.AppView = new AppView;
After reloading the page, I open that dialog from the Javascript console:
$('#dialog').dialog('open')
And I am greeted with a right proper jQuery-ui dialog:
Unfortunately, when I click the "OK" button, nothing happens. Well, the dialog closes (the behavior specified in my jQuery-ui dialog() invocation. But a new Event is not create. Even the console.log() statement is not reached.

Hrm...

Eventually, I track this down to two things. First, I need to set the el attribute to the dialog's parent:
    window.AppView = Backbone.View.extend({
      el: $("#dialog").parent(),
      // ...
   });
This way, the wrapper divs added by jQuery-ui become the element for this view. Also, I need to add a class to the OK button:

script
  $(function() {
    $('#dialog').dialog({
      autoOpen: false,
      modal: true,
      buttons: [
        { text: "OK",
          class: "ok",
          click: function() { $(this).dialog("close"); } },
        { text: "Cancel",
          click: function() { $(this).dialog("close"); } }
      ]
    });
  });
With that, I reach my console.log statement and I try to create my event:


Well, once I create the backend POST route, I ought to able to create appointments.

The POST route in my express.js needs to POST the submitted JSON to CouchDB as 'application/json' data. Thus, my POST route is:
app.post('/events', function(req, res){
  var options = {
    method: 'POST',
    host: 'localhost',
    port: 5984,
    path: '/calendar',
    headers: {'content-type': 'application/json'}
  };

  var couch_req = http.request(options, function(couch_response) {
    console.log("Got response: %s %s:%d%s", couch_response.statusCode, options.host, options.port, options.path);

    couch_response.pipe(res);
  }).on('error', function(e) {
    console.log("Got error: " + e.message);
  });

  couch_req.write(JSON.stringify(req.body));
  couch_req.end();
});
Aside from the headers and the write() of the JSON data to the CouchDB request, the remainder of this route looks very similar to stuff that I have been writing for GETs and DELETEs over the past few days. It may be time to investigate adding an abstraction layer in my express app. Another day, perhaps.

With the backend POST route, I am able to create appointments. I still have a bunch of cleanup to do in this, but I think I am off to a good start. I will pick back up here tomorrow.

Day #130