OdeToCode IC Logo

Deferred and Promises (BYOSP Part 8)

Wednesday, February 29, 2012

When I started thinking about adding animations to the slide presenter I pictured a few different types of animations – simple animations to make items appear one by one, but also animations to pan, zoom, and rotate a slide (like Prezi, which is Flash based, but really more like Impress.js, which is all script and CSS).

I wanted animations to be decoupled and extensible, yet still give the slide presenter control over the animations. When an animation like "appear one by one" is in effect, then a "move forward" operation shouldn't go to the next slide, but should step the animation forward to make an item appear. Once all the items are on the screen then "move forward" can advance the slide. A "move back" should cancel any running animations and go to the previous slide.

unit test warning bellIt's funny – as I was thinking of these features I thought about eventing with jQuery trigger and completion signals with jQuery promises. Then I thought I better keep it simple and plowed ahead using function callbacks for everything.

My unit tests told me the design wasn't working. When small changes make tests break and feel brittle, it is a general indication of trouble – like a warning bell.

With YAGNI disproved I was back to promises and triggers. At the heart of the matter is a completionSignal. All the animation modules rely on completionSignal to broadcast when the animation is finished, and tell them if the animation needs to stop early (the presenter triggers the killAnimations event if the slides need to jump backwards, to the home, or to the end).

var completionSignal = function () {

    var deferred = $.Deferred();

    var complete = function () {
        $(window).unbind("killAnimations", complete);
        deferred.resolve();
    };

    $(window).bind("killAnimations", complete);

    return {
        resolve: complete,
        promise: deferred.promise()        
    };
};

 

The beauty of a deferred object, which typically represents an asynchronous operations,  is how other components can register zero or more functions into callback queues via the object's promise. When the deferred object is resolved (successful completion) or rejected (failed), the object invokes the proper callbacks. We'll see an example in just a bit, but for more, see "Creating Responsive Applications Using jQuery Deferred and Promises". For custom events and trigger see  "Event Aggregation with jQuery".

Simple Animation

To see how animation works in the slide presenter, here is a simple animation that will pop up an alert box for the current slide before allowing the presentation to move to the next slide. Animations are added using data- attributes.

<section>
    <h1>This is the title</h1>
    <ul>
        <li data-animation="alert">Show this in a popup</li>
        <li>More text</li>
    </ul>
</section>

The slide presenter finds the data-animation attributes, then looks up a module to call using the value of the attribute. Multiple animations modules can register with the slide show software.

p5.registerAnimations({
    "onebyone": onebyone,
    "timedAppear": timedAppear,
    "zoom": zoom,
    "addclass": addclass,
    "alert": annoying
});

The "alert" animation is aliased from a module named "annoying" (because any kind of modal dialog is annoying). The annoying module uses a signalCompletion to help with lifetime events.

var annoying = function (element) {
    var signal = completionSignal();

    return {
        step: function () {
            alert(element.text());
            signal.resolve();
        },
        done: signal.promise
    };
};

The public API exposed from any animation requires a step method and done property. The slide show software is responsible for invoking the step method when the slide show tries to move forward. The animation is responsible for resolving the "done" promise when the animation is complete. The animation can also use the signal to register cleanup code (none needed in this example). The presenter software uses the promise with jQuery's $.when to remove completed animations from it's call queue.

$.when(animation.done)
 .then(function () {
     currentAnimations.remove(animation);
 });

 

Does This Painfully Boring Series Ever End?

I'll try to wrap it up next week by looking at only two more pieces: formatting code in a slide, and rotating, panning, and zooming with CSS 3 transforms.

For all the code, see github.