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?
7 comments
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?
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
onLoadingbehavior, 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?
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.
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. :)
John,
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.
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.
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.
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.
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. Therequestmethod could just call:And that could look up responders registered with an object.
Wheels are turning again…thanks John!
Speak your mind: