Monday, October 20, 2008

Delayed selectors in jQuery

On our new redesign we're doing a lot of crazy JavaScript and have come up with some interesting solutions to interesting problems. Today I was working on a problem where hitting the "play" button for a song before the player SWF had loaded caused a JS error on the page. It was trying to call a JS method on the swf, but the method didn't exist yet.

I came up with this interesting solution which is extensible and reusable for a lot of use cases. Here's a concise illustration of the technique:

<html><head><title>Test</title></head><body>
<script src="http://www.google.com/jsapi"></script>
<script>
google.load("jquery", "1");
google.setOnLoadCallback(function() {

DelayProxy = function(sel) {
this.sel = sel;
var self = this;
this.interval = setInterval(function() { self.try(); }, 100);
};

DelayProxy.prototype = {
queue: [],
'__noSuchMethod__': function(id, args) {
this.queue.push([id, args]);
},

'try': function() {
var res = $(this.sel);
if (res.length) {
clearTimeout(this.interval);
for each (var call in this.queue) {
res[call[0]].apply(res, call[1]);
}
}
}
};

$.whenAvailable = function(target) {
var els = $(target);
if (els.length) {
return els;
} else {
return new DelayProxy(target);
}
};

$('#run-test').click(function() {
$.whenAvailable('#target').attr('value', "hello world!");
});

$('#make-target').click(function() {
$('body').append('<input type="text" id="target">');
});

});
</script>

<a href="#" id="run-test">Set target value</a>
<a href="#" id="make-target">Make target</a>

</body>
</html>


You can try this demo here. First click "make target" and then "set target value". Nothing special going on - we're inserting a text field into the DOM, then setting its value using the jQuery attr function. Now reload the page so the inserted field goes away and hit the links in the opposite order. As soon as you hit "make target" the delayed "set target value" call should execute.

The real trick behind this code is the __noSuchMethod__ function. If an object has this property set to a function, it will be called for any missing member function call, and will receive the function name and arguments as arguments. It's similar to PHP's __call or perl's AUTOLOAD and comes in really handy. Here's how the code works:

  • When you call $.whenAvailable, it checks with jQuery to see if the selector has any matching elements. Since it doesn't, it returns a new DelayProxy object. The constructor of the DelayProxy sets up a timer which calls try every 100ms.

  • When you call attr(...) on the DelayProxy, it fires the __noSuchMethod__ function. This pushes onto the internal queue.

  • When you later hit the "Make target" button, the selector becomes valid

  • Some amount of time (<100ms) later, the timer fires and DelayProxy.try finds the selector. This kills itself (clearTimeout) and then applies the queued up function(s) to the resulting selector.



Pretty neat! I used to hate JS, but it does have some pretty powerful tricks.

update: it turns out this is a Mozilla SpiderMonkey extension, and hence not really useful in the real world. Boo!! The same idea still applies, but you've got to manually register the function for each of the functions to be delayed.