I have been running into a lot or problems lately with some of my scripts that rely heavily on Ajax that occurs as soon as the page loads. It seems that during the inital page load, when all of the scripts and stylesheets and images are coming into the browser, running 30-40 XHR requests just isn’t a very good idea. I’m sure it possible to write perfect code that doesn’t contain any race conditions and is careful enough to avoid all problems, but when things get really messy sometimes you just wish you could slow the code a little bit and make things happen sequentially; or at least prevent them from swarming all at once and bringing the client to a crawl.
Introducing nQ, my super-simple enqueuing script for MooTools. The concept is very simple:
You throw partial functions at nQ, which adds them to an internal queue. After 100ms, the function is supposed to execute. After the timer has elapsed, nQ checks the wallclock to see if more than 100ms have ACTUALLY passed since the item was queued - if so, that means that the browser is having trouble keeping up with all the requests. nQ slowly backs off, gradually increasing the timing between successive de-queuing calls, until the wallclock starts to show less burden on the browser. Then nQ starts speeding up again.
There are a couple of cool features in nQ, including an intelligent faculty that allows it to be used in somewhat more complex ways. For example, if you are using nQ as the destiny function for my Aggregator (elsewhere here on Nibbles), your data collection will be passed to the nQ wrapper rather than the intended recipient, the inner partial function. To resolve this situation, any arguments passed to nQ after the partial function will be curried into the partial function prior to de-queuing.
I should mention that this bit of code is very experimental, I only just wrote it today although it does seem to work as expected. nJoy!
// nQ ultra-simple performance-sensitive queueing mechanism.
// ---------------------------------------------------------
QT = null;
Q = [];
nQ = function(work){
/*
* This first if is to roll in extra arguments that may arrive
* at nQ that were really meant for the work function.
*/
if(arguments.length-1)
{
var w = work;
var args = Array.prototype.slice.call(arguments).slice(1);
work = function(){ return w.apply(w, args) };
}
if(!Q)
return work();
Q.push(work);
if(!QT)
dQ.curry($time(), 100).delay(100);
};
dQ = function(start,spacer)
{
//if(window.console) window.console.log( "Spacer now " + spacer );
var now = $time();
if(Q.length)
{
var work = Q.shift(); work();
if(!Q.length) // if that was the last item in the queue
return $clear(QT);
var drift = now - start;
var high = Math.floor(spacer*1.10);
var low = Math.floor(spacer*.90);
if(drift > high)
{
spacer = high;
}
if(drift <= low)
{
spacer = low;
}
dQ.curry(now, spacer).delay(spacer);
}
else
{
return $clear(QT);
}
};
// --------------------------------------------------
// end nQ stuff
Please remember that you’ll also need my Curry prototype to use nQ.
Visitors may have noticed the new “Run This” buttons next to code sections on the Nibbles blog. These buttons allow you to experiment with the code listed inside the box by running it in your browser.
However, remember that in most cases there is code towards the end of the post which makes the previous code possible. If you are inclined to experiment with the code, please remember to run everything in the right order. If there is a problem, the code box will turn red and show an error message which may be helpful in figuring out what you missed.
Thanks!
Tags: JavaScript, meta
Curry is one of my favorite functions - it’s one of those indispensable tools that you never know about until you need it, and then in a flash you realize all of the potential applications for.
First of all, I need to give credit where credit is due. I learned about curry() while using Dojo, and was shocked to find out that MooTools’ pass() didn’t do the same thing. So special thanks to Alex Russell of Dojo and the other dude whose code I lifted and adapted for use in MooTools.
What is a Curry?
Aside from being a delicious spicy Thai mainstay, it’s also the process of wrapping a function inside another function, such that some of the inner functions’ arguments have already been defined.
Say What?
Let’s say you have a function printName, which expects lastName and firstName as arguments. To whit:
printName = function(lastName, firstName){
alert("Hello, " + firstName + " " + lastName);
}
Now let’s say you have a list of names that are in a Hash, like this:
names = $H({
'Jones': ['Barnaby','Sally','Timmy','Bridget'],
'Smith': ['Gabby', 'Kelly', 'Crabby']
});
Here’s how a curry can help:
names.each( function(firsts, lastName){
var fn = printName.curry(lastName);
firsts.each( fn );
})
Whoa.
What’s happened above is that we’ve walked through the last names object, which gives us a list of the first names (’firsts’) and a string key which is the last name (’lastName’). Next, we create a temporary function ‘fn’ that is a copy of printName, but with the first argument (lastName) already filled in. Next, we pass each of the first names in turn to the temporary function. Since the first argument that each() passes is the value of the array item (the first name), we are effectively calling printName with both the first argument (from the curry) and the second argument (from each()) filled out.
In this case, curry() helped us to avoid a potentially inelegant series of for() loops. We could even state the same code in a simpler (albeit more opaque) manner:
names.each( function(firsts, lastName){
firsts.each( printName.curry(lastName) );
});
How does it work?
Curry() takes a function’s argument list and holds it in a closure, which it returns. The big difference between my MooTools curry() and MooTools’ own pass() is support for multiple levels of curry. Once you call pass(), calling pass() again on the result won’t get you anywhere. My curry() can attach as many arguments as you want. Another difference is that pass() takes an array as it’s first argument if you want to pass multiple arguments. For curry(), just call it as if it were the target function and the args will be converted to an array for you.
Function.prototype.curry = function()
{
var method = this, args = Array.prototype.slice.call(arguments);
return function()
{
return method.apply(this, args.concat(Array.prototype.slice.call(arguments)));
};
};
Tags: abstract, advanced, mootools
In our increasingly Ajax-centered world, where things happen in an unpredictable order, it is often very important to make sure some list of things has occurred prior to taking an action. This can be especially painful, for example, when multiple XHR events are pending and a final action relies on them all being completed in no particular order.
Presenting Aggregator, a deceptively simple little widget with a lot of firepower. The concept is as follows:
The Aggregator is called like this:
Collector = Aggregator( {stages: ['first', 'second']}, alert.pass('all done'));
Collector is a closure that provides the aggregating functionality. You use Collector like this:
Collector( 'first' );
This toggles the first stage switch and runs the latch. If the latch returns true, the destiny function is called. In this case, since we didn’t specify a latch, the default is being used. Therefore, the destiny function will not yet call because ’second’ has not yet been satisfied.
Asset.javascript( 'somescript.js', {onload: Collector.pass('second') });
You can cheat and run the second step below.
Collector('second');
When the javascript file is loaded, the Collector function will be called with ’second’ as it’s argument. When that happens, the latch will be satisfied and the destiny function alert(’all done’) will be called.
The Aggregator can also collect data. A good example is when using Google Maps API. Let’s say you want to wait until an address has been geocoded, an image has been loaded, some data has been loaded, and a few other operations are done before calling an “addMarker” function:
var gc = new API.ClientGeocoder;
var markerBuilder = Aggregator({
stages: ['placemarks', 'markeropts', 'markerdata', 'nodata', 'image'],
latch: function(s){
with (s)
return placemarks && markeropts && image && (markerdata || nodata);
}
}, addMarker);
/* Step 1: Start Geocode */
gc.getLatLng(address, markerBuilder.curry('placemarks'));
/* Step 2: Fetch Marker */
var markerOpts = {
clickable: true,
draggable: (opts ? (opts.canmove ? true : false) : false),
minZoom: (opts ? (opts.weight ? opts.weight : 1) : 1)
};
markerBuilder('markeropts', markerOpts);
/* Step 3: Fetch Category Data */
if (categoryId) {
// hypothetical data loader "DataAdapter"
var MarkerData = new DataAdapter('Marker', categoryId);
MarkerData.get(markerBuilder.curry('markerdata'));
}
else {
markerBuilder('nodata');
}
/* Step 4: Image */
var Image = new Asset.image('/someimage.jpg', {onload: markerBuilder.curry('image',Image)})
The appeal of this approach is immediately obvious to anyone who regularly needs to dispatch multiple concurrent operations using XHR or similar asynchronous processes.
The full code for Aggregator is very small, as you can see:
Aggregator = function(config, finalfn)
{
var latch = null; var priv = {}; var state = $H({});
// first, assign the latching semantics.
// either use the configured latch function or a simple one.
// the simple latch below examines each member of 'state' to see if it
// is defined. If not, it quits. If the latch gets all the way through,
// it succeeds.
latch = config.latch ? config.latch : ( function(state,priv){
// release when all values are defined.
return ! (state.contains(false));
});
// next, set up the private space for stages and the latch state for each.
$each(config.stages || config, function(v){
state[v] = false;
priv[v] = '';
});
// next, define the closure.
var r = function(stage, data){
if (stage && !state[stage]) {
priv[stage] = data;
state[stage] = true;
}
if(latch(state,priv))
finalfn(priv);
}
return r;
}
Note that if you want to pass data values to an Aggregator lambda-function, you also need my Function.curry definition, which allows you to build functions with a built-in argument list. Curry is posted on the Nibbles and Bits blog as well.
Tags: abstract, advanced, ajax, mootools, xhr
We all know what a pain it can be to design certain elements, like tabs or menu graphics, that can expand to suit any length of input. Of course, we do our best to accomodate, but some content is just too long to look good.
My solution to this is TextScroller - you give it a list of elements to parse, and if any of them have text longer than a threshold amount, it will truncate the text with ‘…’ But that’s not all. When the user mouses over, the text will seem to ’scroll’ through the element until the entire string has been displayed.
Features:
Example:
The text in the box above is too long to fit in the box. Click below to turn on TextScroller
/** * @author David Baltusavich * @license GPLv2 * @copyright 2008 SynaTree Internet Growth Strategies, all rights reserved. * * TextScroller, a simple routine to truncate and scroll long strings inside elements. */
TextScroller = new Class({
/**
* Create a scroller if the content in the target node is sufficiently long.
* @param {DOMNode} node
* @param {Integer} maxLen, the maximum length of textContent before triggering the routine
* @param {Integer} increment, the number of characters to consume each iteration
* @param {Integer} rate, a timer number that is the basis for the periodical and delays.
*/
initialize: function(node, maxLen, increment, rate){
var bLong = false;
var bActing = false;
increment = increment || 3;
maxLen = maxLen || 50;
rate = rate || 200;
var pauserate = rate * 3;
var trumaxLen = maxLen - 3;
var timers = {start: false, end: false};
var ticker = 0;
var reset = function(){
node.set('text', cache.slice(0, trumaxLen) + '...');
ticker = 0;
};
if(node.get('text').length > maxLen )
{
bLong = true;
var cache = node.get('text');
reset();
}
if(bLong)
{
var start = function(){
ticker += increment;
// if we are not yet at the end, show a little more of the string.
var bEnough = cache.length > ticker+trumaxLen;
// if there is still even more, keep the dots, otherwise, just show the whole string.
node.set('text', bEnough ? cache.slice(ticker, ticker+trumaxLen) + '...' : cache.slice(ticker) );
if(!bEnough)
// we've come to the end, stop calling the scroller.
$clear(timers.start);
};
node.addEvent('mouseover', function(){
// cancel any pending reset timer
if(timers.end)
{
$clear(timers.end);
delete timers.end;
}
// start the scroller
if(!timers.start)
{
timers.start = start.periodical(rate);
}
} );
node.addEvent('mouseout', function(){
// wait a moment in case the user get back inside the box, then reset the text to it's original state.
if(timers.start)
{
timers.end = (function(){
$clear( timers.start );
delete timers.start;
reset();
}).delay(pauserate);
}
} );
}
}
});
/**
* A handy utility to scan a collection of elements
* @param {Collection} coll, a collection of Nodes.
* @param {Integer} maxLength, the maximum allowed length of textContent
* @param {Integer} time, basic timer interval (speed)
* @param {Integer} increment, the number of characters to consume with each iteration
* @returns {Array} array of scroller objects.
*/
TextScroller.scanElements = function( coll, maxLength, time, increment)
{
var scrollers = [];
maxLength = maxLength || 50;
time = time || 200;
increment = increment || 3;
if(coll.each)
coll.each(function(item){
scrollers.push( new TextScroller(item, maxLength, increment, time) );
});
return scrollers;
};