OdeToCode IC Logo

Event Aggregation with jQuery

Tuesday, July 7, 2009

As the “write less, do more” library, jQuery garners lots of love for its terseness. The terseness, combined with a rich ecosystem of plug-ins, means I can display my OdeToCode twitter feed on a web page using only 10 lines of code (complete with animation and custom loading message)*.

$(function() {
    $("#getTweets").click(function() {
        $("#tweets").getTwitter({
            userName: $("#screenname").val(),
            numTweets: 10,
            loaderText: "Fetching tweets...",
            slideIn: true,
            showHeading: true,
            headingText: "Latest Tweets",
            showProfileLink: true
        });
        return false;
    });
});

When writing with jQuery there is a tendency to use nested functions and collapse as many responsibilities as possible into a single piece of code. For many web pages this approach is entirely suitable.We aren’t building a space shuttle - it’s just a web page. The code above is responsible for locating the DOM element for events, hooking up a click event, fetching tweets, and locating the DOM element to display the tweets.

Composite UIs

In more complex pages, and particularly in composite pages that are made up from independent pieces, the above approach tends to become brittle, and encapsulation breaks down as independent pieces try to peek into each other’s private business. It’s easy to fall into black hole of JavaScript code that swallows all who come near. A step away from the black hole would be to extract some of the common, reusable functionality into different pieces.

For example, you can separate the piece that knows about DOM elements …

$(function() {
    $("#getTweets").click(function() {
        getTweets($("#tweets"), $("#screenname").val());
        return false;
    });
});

… from the piece that knows about Twitter, and include the pieces independently …

function getTweets(element, screenname) {
    $(element).getTwitter({
        userName: screenname,
        numTweets: 10,
        loaderText: "Loading tweets...",
        slideIn: true,
        showHeading: true,
        headingText: "Latest Tweets",
        showProfileLink: true
    });
}

Now we have a bit of separation. At this point some of us would be inclined to raise the battle cry of the object oriented programmer, and run off to a workstation to design  namespaces, prototypes, constructor functions,  properties – blah blah blah**. But we wouldn’t be creating a greater separation between the pieces of code. All we’d really be doing is creating bigger abstractions that are still tied together as closely as they were when they were simple function objects.

Enter The Aggregator

One of the classes I dig in Prism is the EventAggregator. Eventing is pretty much a required approach to managing a composite UI if you want to stay sane. The EventAggregator makes this easy in WPF and SIlverlight, and includes some thread marshalling tricks behind the scenes as a bonus.

Fortunately, you can do something similar in most major JavaScript frameworks, including jQuery. There are jQuery plugins dedicated to event aggregating, but a simple approach would use bind and trigger with custom events, letting the document serve as the aggregator.

Now we can achieve a greater decoupling between the “I need tweets” action …

$(function() {
    $("#getTweets").click(function() {
        $(document).trigger("fetchTweets", [$("#tweets"), $("#screenname").val()]);
        return false;
    });
});

… and the piece (or pieces) that will respond to such an action…

$(document).bind("fetchTweets", function(e, element, screenname) {
    $(element).getTwitter({
        userName: screenname,
        numTweets: 10,
        loaderText: "Loading tweets...",
        slideIn: true,
        showHeading: true,
        headingText: "Latest Tweets",
        showProfileLink: true
    });
});

It’s easy to layer in additional behavior to the “Get Tweets” button click. We could call a web service to save the user’s preferences. We could cache information. We could add some debugging info …

$(document).bind("fetchTweets", function(e, element, screenname) {
    console.log(screenname);
});

All these things could be done by including separate scripts, and without putting any knowledge of these actions inside the click event handler where the aggregated event begins. Of course, to complete the circle, the code should raise an event when the tweets are retrieved, and let someone else deal with the results.

* Most excellent Twitter plug-in is available from @code_za’s blog.

** I’m not saying that bending prototypes to act like classes is bad, it’s just not the solution to every problem.