Where did all my code go? Using Ojay chains to express yourself clearly

I’ve been putting together a presentation to be given internally at work on what Ojay is and why we’re doing it. It occurred to me that I’ve not spoken very much about it here, hoping the documentation and examples would speak for themselves. So, today I’m going to go through how to take an animation sequence that would be really complicated in YUI and make it so simple that you’ll never want to write another callback function again.

The really core thing about Ojay’s chaining API is that each function that does something asynchronous (like an animation, or an HTTP request) returns a MethodChain object, which is essentially a magic object that remembers methods called on it so they can be replayed later. This allows you to construct complex instruction sequences with far fewer (sometimes none!) nested callback functions.

For today’s example, let’s assume you want to do the following. You have an element called trigger and another called logo. When you click the trigger, you want the logo to move around the edges of square. During the animation, the logo should pause for half a second at each corner. So, your starting point might be:

<div id="trigger"></div>
<div id="logo"></div>

<script type="text/javascript">
    YAHOO.util.Dom.setStyle('logo', 'position', 'absolute');
    YAHOO.util.Dom.setStyle('logo', 'left', '200px');
    YAHOO.util.Dom.setStyle('logo', 'top', '200px');

    var points = [[400,200], [400,400], [200,400], [200,200]];
</script>

The question is, how are you going to implement the animation sequence? In this example, I’m going to show how this case can be simplified using loops, but other sequences of events don’t lend themselves to this simplification. My hope is to show that Ojay scales to very complicated asynchronous sequences without making your head (or your typing fingers) hurt.

Let’s try a really dumb implementation. And, remember that this is only dumb because this case lends itself easily to looping. With more complex effect combinations you won’t be so lucky. The dumb implementation is to simply list all the animations by hand, chaining them together using onComplete callbacks. We use setTimeout() to handle the pauses.

YAHOO.util.Event.addListener('trigger', 'click', function() {
    var anim = new YAHOO.util.Anim('logo', {
        left:   {to: points[0][0]},
        top:    {to: points[0][1]}
    }, 0.7);
    anim.onComplete.subscribe(function() {
        setTimeout(function() {
            var anim = new YAHOO.util.Anim('logo', {
                left:   {to: points[1][0]},
                top:    {to: points[1][1]}
            }, 0.7);
            anim.onComplete.subscribe(function() {
                setTimeout(function() {
                    var anim = new YAHOO.util.Anim('logo', {
                        left:   {to: points[2][0]},
                        top:    {to: points[2][1]}
                    }, 0.7);
                    anim.onComplete.subscribe(function() {
                        setTimeout(function() {
                            var anim = new YAHOO.util.Anim('logo'
                            , { left:   {to: points[3][0]},
                                top:    {to: points[3][1]}
                            }, 0.7);
                            anim.animate();
                        }, 500);
                    });
                    anim.animate();
                }, 500);
            });
            anim.animate();
        }, 500);
    });
    anim.animate();
});

There are several things wrong with this, aside from the fact that it just looks wrong because of all the repetition. First up, it doesn’t tell a story, at least not one that’s easy to follow. Instead of flowing down the page, it flows outwards in this nested structure, so that pause intervals are specified nowhere near where the pause actually happens in the sequence. Similarly, the call to initiate each animation is miles away from the code that runs after the animation. To follow the story, you have skip up and down the text. You can make this easier by specifying each callback externally and chaining them afterwards; the event handler will tell a better story but the chain will probably be even harder to follow.

So, let’s try a more digestable approach by turning this into a loop. If this really didn’t lend itself to looping, then we’d be stuck with the above code, but we can make this case shorter so let’s have a go. We’ll make a loop that kicks off the four stages of the animation, but we don’t want all of them to run at once so we’ll delay each stage by an appropriate amount such that the whole effect works properly.

YAHOO.util.Event.addListener('trigger', 'click', function() {
    for (var i = 0, n = points.length; i < n; i++) {
        (function(x) {
            setTimeout(function() {
                var anim = new YAHOO.util.Anim('logo', {
                    left:   {to: points[x][0]},
                    top:    {to: points[x][1]}
                }, 0.7);
                anim.animate();
            }, (700 + 500) * x);
        })(i);
    }
});

This is just about the shortest way I managed to write this code using YUI. It perhaps looks nicer than the first example, but it’s broken in a pretty fundamental way: it works sort of ‘by accident’ in that you happen to have timed everything just right so that it does what you expect. You’ve not actually chained the animations together using callbacks, so each one runs regardless of whether the one before it has actually finished. This is especially important if there are other side effects of the animation running, if other actions need to occur at the same time. If one step fails, the next step will still run. There are some other issues, like that funny-looking closure to make sure the correct step runs, and the whole thing isn’t that readable really.

So, you need to chain the steps together properly. We can still do this using a loop by building an array of animations, and when each one finishes it shifts the next animation off the stack and runs it:

YAHOO.util.Event.addListener('trigger', 'click', function() {
    var anim, anims = [];
    for (var i = 0, n = points.length; i < n; i++) {
        anim = new YAHOO.util.Anim('logo', {
            left:   {to: points[i][0]},
            top:    {to: points[i][1]}
        }, 0.7);
        anim.onComplete.subscribe(function() {
            var nextAnim = anims.shift();
            if (!nextAnim) return;
            setTimeout(function() {
                nextAnim.animate();
            }, 500);
        });
        anims.push(anim);
    }
    anims.shift().animate();
});

This has made the code longer, but at least it’s robust now: the start of each animation is properly synchronized with the end of the previous one. This is the shortest robust implementation that I could find in YUI and Plain Old JavaScript. It might be called elegant but I’m not sure it’s that much easier to follow than the first huge example. At least it’s explicit about the fact that animations are chained, which makes it better than the previous snippet.

Now, let’s use Ojay to translate our ‘dumb’ example from above:

$('#trigger').on('click')
    ._('#logo')
    .animate({
        left:   {to: points[0][0]},
        top:    {to: points[0][1]}
    }, 0.7)
    .wait(0.5)
    .animate({
        left:   {to: points[1][0]},
        top:    {to: points[1][1]}
    }, 0.7)
    .wait(0.5)
    .animate({
        left:   {to: points[2][0]},
        top:    {to: points[2][1]}
    }, 0.7)
    .wait(0.5)
    .animate({
        left:   {to: points[3][0]},
        top:    {to: points[3][1]}
    }, 0.7)
    .wait(0.5);

I don’t expect I’ll meet much opposition if I suggest that this is incredibly readable. There is no cruft aside from the punctuation required to make JavaScript parse the code correctly. It tells a nice linear story, where each stage and parameter is mentioned in the order the sequence actually takes place. There is no nesting – one mantra I have in my mind pretty often is a snippet from ‘Why Why Functional Programming Matters Matters’: “there’s nothing inherently nested about what we’re trying to do”. And, it’s actually shorter than the final YUI attempt above (both with whitespace removed).

And if you feel like going super-minimal, Ojay includes the JavaScript 1.8 Array#reduce() function that makes shrinking the chain really simple:

$('#trigger').on('click', function() {
    points.reduce(function(chain, point) {
        return chain.animate({
            left:   {to: point[0]},
            top:    {to: point[1]}
        }, 0.7).wait(0.5);
    }, $('#logo'));
});

Back to my assertion that Ojay scales better than other libraries to complex sequences. This example was fairly contrived, mostly for sake of focusing on the code rather than the application. I could just as well have illustrated all this with this example: when we click a button, fade in a ‘loading’ indicator, make a search request to the server, print the result to the page and fade out the loading indicator. This would be an irreducible pile of nested callbacks in YUI, and indeed in Prototype or jQuery. In Ojay:

var getSearchTerm = function() {
    return $('#q').node.value;
};

$('#search').on('click')
    ._('#loading').animate({opacity: {to: 1}}, 0.3)
    ._($.HTTP).GET('/search', {q: getSearchTerm})
    .insertInto('#results')
    ._('#loading').animate({opacity: {to: 0}}, 0.3);

Notice how $.HTTP.GET can take functions as query parameters: the getSearchTerm function returns the value of an input field at the time the event fires. The equivalent YUI code, just for completeness:

YAHOO.util.Event.addListener('search', 'click', function() {
    var anim = new YAHOO.util.Anim('loading', {
        opacity:  {to: 1}
    }, 0.3);
    anim.onComplete.subscribe(function() {
        var term = YAHOO.util.Dom.get('q').value;
        YAHOO.util.Connect.asyncRequest('GET',
            '/search?q=' + term, {
            success: function(response) {
                YAHOO.util.Dom.get('results').innerHTML =
                        response.responseText;
                var anim = new YAHOO.util.Anim('loader', {
                    opacity:  {to: 0}
                }, 0.3);
                anim.animate();
            }
        });
    });
    anim.animate();
});

This is basically what I mean when I say that Ojay scales: it doesn’t get confusing when you add more and more async behaviour. In fact, Ojay code doesn’t really get more complicated at all in the sense that YUI does; it might get bigger, but it remains easy to understand.