opensoul.org

Observers for Ajax callbacks

In my Javascript-fu adventures over the past week or so, I’ve consistently run into the same problem. I want to do slight variations of an Ajax request on an individual basis. Here is a common method from my code: (Note: these examples use Event.addBehavior from Dan Webb’s excellent lowpro library for registering events.)

Event.addBehavior({
  // hijack any forms with the class "new" and submit them using Ajax
  'form.new:submit': function(event) {
    this.request({evalScripts: true,
      onLoading: function() { this.disable(); }.bind(this),
      onComlete: function() { this.enable(); }.bind(this)
    });
    Event.stop(event);
  }
});

But occasionally, I want a certain form to do something slightly different. For example, I have some forms that are hidden by default and I want to hide them again after they are submitted:

Event.addBehavior({
  'form.new.hidden:submit': function(event) {
    this.request({evalScripts: true,
      onLoading: function() { this.disable(); }.bind(this),
      onComlete: function() { this.enable(); }.bind(this),
      onSuccess: function() { new Effect.BlindUp(this); }.bind(this)
    });
    Event.stop(event);
  },
})

The problems with this are, 1) there is a lot of duplication, and 2) if this form has the “new” class name, it’s going to end up with 2 event handlers registered, both making an Ajax request.

So what I want is event observers on Form.request and Anchor.request for the Ajax request lifecycle, so I can call form.request.observe('loading', function() { … }), and that will be invoked any time an Ajax request is made for that form:

Event.addBehavior({
  // submit form requests using ajax
  'form.new:submit': function(event) {
    this.request({evalScripts: true});
    Event.stop(event);
  },

  // register observers to disable and enable the form
  'form.new': function() {
    this.request.observe({
      'loading': function() { this.disable(); }.bind(this),
      'complete': function() { this.enable(); }.bind(this)
    });
  },

  // register an observer to hide the form on success
  'form.new.hidden': function() {
    this.request.observe('success', function() {
      new Effect.BlindUp(this);
    }.bind(this));
  }
})

And this is where I need your help.

The first problem is that we need some type of event notification framework. I have that part solved by using a slightly modified version of Ryan Johnson’s Object.Event library, which basically allows you to add event observers to any object, and then call them by calling notify('eventname') within the object.

Next, I’ve made a modified version of Anchor.reqeust method from my post yesterday, which calls notify on each of the Ajax callbacks, and extends the request methods with the event notification methods:

if (!window.Anchor) var Anchor = new Object();

Anchor.Methods = {
  request: function(anchor, options) {
    anchor = $(anchor)
    callbacks = {}
    $A(['loading', 'complete', 'exception', 'failure', 'success']).each(function(event) {
      callbacks['on' + event.capitalize()] = function() {
        anchor.notify.apply(anchor, arguments.unshift(event));
      };
    });
    options = Object.extend(Object.extend({
      method: 'get'
    }, callbacks), options || {});
    return new Ajax.Request(anchor.readAttribute('href'), options);
  }
}
Object.Event.extend(Anchor.Methods.request);
Element.addMethods('a', Anchor.Methods);

This is where the second problem comes in. While the event notification methods are added to Anchor.Methods.request, they don’t get added to the anchor objects when the request method does. They’re getting lost in Prototype’s Element.extend method that adds the extensions to each element.

And as soon as I get that problem solved, I’m going to have another one: one instance of Anchor.request will be shared amongst all the anchor objects, so registering an observer on one will register it for every Ajax request. What I need then is for the event observer to keep track of the registered observers in each object, even though the methods are on the shared register function.

I could move the event registration methods to the anchor instead of on the request method, but that has problems of it’s own. Namely, each element already has an observe method for the browser’s events, and I can’t come up with a better name than observe. Besides, I think the methods belong on the request object; they are specific to the Ajax request.

So, does anyone have any good ideas? Is this a lame/unnecessary feature?

ajax, events, javascript, and prototype July 12, 2007

7 Comments

  1. Matt Jones Matt Jones July 12, 2007

    Are you making calls to an external resource (that you
    don’t have control over)? If not, then this is usually done
    with a JSON reply (for example, using RJS in Rails).

    Essentially, the server reply is responsible for hiding the
    form (using the Prototype effect).

    Maybe I’m missing the point – would this work?

  2. Brandon Brandon July 13, 2007

    Matt,

    That would work in some situations, and is in fact what I’m doing for the time being. But it leads to a ton of duplication. In several apps I’ve found myself using the same patterns, and duplicating the same code across all my rjs templates (helpers help with the duplication).

    But the most common situation that I’m running into is wanting to change the onLoading behavior, which obviously can’t be done through the response. For some forms I only want it to change the button to “saving”, some forms I want it to gray out the element that is being updated, etc.

    Does that make sense? Am I making this more difficult than it needs to be?

  3. John Nunemaker John Nunemaker July 13, 2007

    I don’t know if I have totally thought this through and it is 7:30AM but what about using classes on the a or form. So on loading you want to call this.disable() so you classify it loading_disable. You could loop through the classes on an element before making the request:

    callbacks = {}
    valid_callbacks = $A([‘loading’, ‘complete’, ‘exception’, ‘failure’, ‘success’]);

    this.classNames.each(function(class) {
    var class_split = class.split(‘’);
    var possible_callback = class_split.first();
    if (valid_callbacks.include(possible_callback)) {
    callbacks[‘on’ + possible_callback.capitalize()] = function() {
    this[class
    split.shift.join(‘-’).camelize()];
    }.bind(this);
    }
    }.bind(this));

    So if you had loading_disable, this.disable would be called and if you had loading_disable_my_poo, this.disableMyPoo would be called. Granted I’m sure my code is off and you also might want to join all the loading events together in one function and the same with all the other callbacks to allow for using loading_thing and loading_another and not have them overwrite each other but I think it could work.

  4. John Nunemaker John Nunemaker July 13, 2007

    Have you looked through the Ajax.Responders code in prototype? You could create something along those lines that only adds the responders to particular elements.

    This is a sweet problem. I wish I had time to prototype and try some solutions. That’s why I keep commenting. :)

  5. Brandon Brandon July 13, 2007

    John,

    what about using classes on the a or form

    Yeah, I thought about that, but I think it’s a better design to have them off the request methods, since they’re specific to the Ajax request. But, if all else fails, this will work.

    Have you looked through the Ajax.Responders code in prototype? You could create something along those lines that only adds the responders to particular elements.

    The problem with responders is that they don’t know anything about the element that triggered the request. Usually, when I want to add behavior, I want the behavior to act on the element, and not the request.

    This is a sweet problem. I wish I had time to prototype and try some solutions. That’s why I keep commenting. :)

    By all means, keep commenting. I’ve been thinking about this and trying different things for a couple days and haven’t come up with a clean solution. All I know is it is something I’m struggling with in every unobtrusive Ajax app that I’ve worked on lately.

  6. John Nunemaker John Nunemaker July 13, 2007

    The problem with responders is that they don’t know anything about the element that triggered the request. Usually, when I want to add behavior, I want the behavior to act on the element, and not the request.

    Yep, understood. I wasn’t clear on that. What I meant was using that as a starting point, you could create something that allowed registering responders for particular elements.

  7. Brandon Brandon July 13, 2007

    John,

    Hmm, good thinking. I don’t know why I didn’t think about the idea of having an external “event registry”. I was thinking that it needed to be on the actual object. This solves the last problem of all the objects sharing an instance of request. The request method could just call:

    Ajax.Responders.dispatch(this, 'onCallback', …);
    

    And that could look up responders registered with an object.

    Wheels are turning again…thanks John!

Post a Comment

Comments use textile. Anonymous comments will be deleted.

My name is Brandon Keepers. I like to build things, usually in Ruby or JavaScript. I work at GitHub and live in Holland, MI.

Popular Posts