Wednesday, October 5, 2011

Rails like Error Handing in Backbone.js

‹prev | My Chain | next›

Tonight, I continue my exploration of validations in Backbone.js.

Last night, I came up with a decent solution for a jQuery UI dialog. When creating a new appointment in my Backbone calendar, callbacks on the create() grab validation errors to provide useful error messages for the user:
First up tonight, I do the same for editing existing appointments. I have a separate dialog for editing (it just seemed easier that way), so I add an error <div> to that dialog (using jade templating):
#edit-dialog(title="Edit calendar appointment")
  h2.startDate
  div.errors(style="background:yellow; display: none")
  p title
  p
    input.title(type="text", name="title")
  p description
  p
    input.description(type="text", name="description")
Since Backbone will handle the success and failure of submitting the form, I remove the OK button's click handler:
  $(function() {
    $('#edit-dialog').dialog({
      autoOpen: false,
      modal: true,
      buttons: [
        { text: "OK",
          class: "ok" },
        { text: "Cancel",
          click: function() { $(this).dialog("close"); } }
      ]
    });
  });
The jQuery dialog still retains control of the Cancel button.

Lastly, I add the optional options argument to the save() call in the Backbone view's update handler:
        var AppointmentEdit = new (Backbone.View.extend({
          // ...
          events : {
            'click .ok': 'update'
          },
          update: function() {
            var attributes = {
              title: $('.title', '#edit-dialog').val(),
              description: $('.description', '#edit-dialog').val()
            }

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

            this.model.save(attributes, options);
          }
        }));
The options hash contains success and error callbacks for save(). If save() is successful, then the success() callback closes the edit jQuery dialog. If save() fails, then the error callback is responsible for placing the error messages inside the <div class="errors"> tag that I added to my jQuery UI dialog.

With that, I have validations working in my edit-appointment views as well:

That is all well and good, but I think that I might like to have some Rails-like validation reporting—full messages plus highlighting individual fields that have failed validations. For that, I need an Errors class that very roughly approximates what the Rails' Error class does:
      var Errors = function(options) {
        options || (options = {});
        this.on = options;
      };

      _.extend(Errors.prototype, {
        isEmpty: function() {
          return (_.size(this.on) === 0);
        },
        add: function(attribute, error) {
          this.on[attribute] = error;
        },
        each: function(callback) {
          _.each(this.on, callback(attribute, error));
        },
        full_messages: function() {
          return _.map(this.on, function(error, attribute) {
            return attribute + " " + error;
          });
        }
      });
Now I can switch my model over to use this error object:
       var Models = (function() {
        var Appointment = Backbone.Model.extend({
          // ...
          validate: function(attributes) {
            var errors = new Errors();

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

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

            if (!errors.isEmpty())
              return errors;
          },
          // ...
        });
And, lastly, I convert my appointment edit View to pull the error messages out of the full_messages method:

        var AppointmentEdit = new (Backbone.View.extend({
          // ....
          events : {
            'click .ok': 'update'
          },
          update: function() {
            var attributes = {
              title: $('.title', '#edit-dialog').val(),
              description: $('.description', '#edit-dialog').val()
            }

            var options = {
              success: function() { $('#edit-dialog').dialog("close"); },
              error: function(model, errors) {
                var full_messages = errors.full_messages();

                $('.errors', '#edit-dialog').
                  html(full_messages.join("<br/>")).
                  show();
              }
            };

            this.model.save(attributes, options);
          }
        }));
With that, I have my dialog working just as before, but with the rails like error handling in place. I call it a night here and will pick back up tomorrow decorating the individual fields with highlights when they fail validations.


Day #154

1 comment:

  1. I couldnt get your example to work until i did this:

    each: function(callback) {
    _.each(this.on, callback(attribute, error));
    },

    TO

    each: function(callback) {
    _.each(this.on, function(error, attribute) {
    callback(error, attribute);
    });
    }

    ReplyDelete