21 May 2010

Improving JavaScript code with custom events

One of the frequently-cited problems with JavaScript is its lack of support for modularisation. It’s particularly problematic now that web applications are becoming more interactive. Structuring a number of AJAX calls and related functions proves to be a big problem if you want to avoid using the global namespace yet want to split your logic into separate files or functions without a shared scope.

There are several frameworks that attempt to address this and features have been proposed for the next version of ECMAScript. However, one simple solution we’ve been experimenting with on the Confluence team is using the browser’s built-in event system — underneath the event abstraction provided by jQuery — to modularise operations in large JavaScript features. That has enabled us to split up our JavaScript logic much more easily and reduce the number of JS files with more than 100 lines of difficult-to-follow script.

A bit of JavaScript that sends off an AJAX request, processes it and also responds to a user’s action on the form might look like this:

jQuery(function ($) {
    function addLinkToList(link) {
        $("#link-list").append($(AJS.template.load("link-item").fill(link));
    }

    $.ajax({
        url: '/get-links.action',
        data: { ... },
        dataType: 'json',
        error: function (xhr, message) {
            alert("Failed to retrieve links: " + message);
        },
        success: function (data) {
            $.each(data.links) {
                addLinkToList({
                    name: this.name,
                    url: this.url
                });
            }
        }
    });

    $("#add-link-button").click(function () {
        $.ajax({
            url: '/add-link.action',
            data: { ... },
            dataType: 'json',
            error: function (xhr, message) {
                alert("Failed to add link: " + message);
            },
            success: function (data) {
                addLinkToList({
                    name: data.name,
                    url: data.url
                });
            }
    });
});

This example demonstrates the fact that both AJAX operations – one that runs initially and one that runs when the user clicks a button – rely on the same function, addLinkToList. This tight coupling gives you very few options for modularising this code in JavaScript if these functions grow large and would best be separated into different files. Most often we’ve ended up making shared functions accessible in the global scope or assigning them to an existing global object (like a DOM element) for subsequent retrieval. Neither option is particularly clean.

The new method we’re experimenting is to use event handling to allow simpler modularisation and data passing between unrelated pieces of JavaScript code.

Here is the same code using jQuery’s event handling to publish a “add-list-item” event when the AJAX calls return:

jQuery(function ($) {
    $(window).bind("add-list-item.link-list", function (e, data) {
        $("#link-list").append($(AJS.template.load("link-item").fill(data));
    });

    $.ajax({
        url: '/get-links.action',
        data: { ... },
        dataType: 'json',
        error: function (xhr, message) {
            alert("Failed to retrieve links: " + message);
        },
        success: function (data) {
            $.each(data.links) {
                $(window).trigger("add-list-item.link-list", {
                    name: this.name,
                    url: this.url
                });
            }
        }
    });

    $("#add-link-button").click(function () {
        $.ajax({
            url: '/add-link.action',
            data: { ... },
            dataType: 'json',
            error: function (xhr, message) {
                alert("Failed to add link: " + message);
            },
            success: function (data) {
                $(window).trigger("add-list-item.link-list", {
                    name: data.name,
                    url: data.url
                });
            }
    });
});

The difference is subtle: instead of calling the function directly, the AJAX functionality calls $(window).trigger("event-name.namespace") and the functionality which updates the list is bound with $(window).bind("event-name.namespace"). The data is passed to the event handler as the second argument of the call to trigger.

The code isn’t any shorter, but it is significantly less tightly coupled. You can now move each block of code into a separate file because they no longer need direct access to a shared function.

The event dispatch in this case is done through the global window object. Custom events don’t propagate through the DOM tree the same way as normal browser events, so it’s necessary to pick a shared object for use in custom event dispatch. If the events are specific to a certain area of the page, you could use a specific element instead — it just has to be consistent between the code triggering and the code receiving the event.

We could continue this approach with the code above by also extracting the common error handling functionality into an event handler. As more dynamic functionality is added, and more code required, you can add new events to handle these changes.

Event handling like this is provides much more modularity than the approach of using shared functions. The event handling model matches very closely with the way AJAX and user-driven functionality works in the browser and in the short time we’ve been using it we’ve found it a great way to improve the modularity of our JavaScript code.

My colleagues Agnes and Dmitry developed the proof-of-concept and initial implementation of this idea in Confluence. Any mistakes in the description above are mine.