jQuery Best Practices Part 2: Chaining

In Part One, I discussed managing the scope of $(document).ready(). Next comes the challenge of organizing the contents of $(document).ready() to balance efficiency and maintainability. My team accomplishes this by taking advantage of the Array behaviors of jQuery to structure our code so that function dictates form. Put into practice, these behaviors will structurally organizing the code without imposing rules and processes—because we all know just how effective rules and processes are.

Is a jQuery Object an Array?

For any selection that you make, such as $('.button'), a jQuery object is returned. This allows you to do a variety of tasks based on what is returned in the selector.

if ($('.button').length > 0) {
...
}

You can also iterate through the contents of the jQuery object:

var selection = $('.button');
foreach(i = 0; i < selection.length; ++i) {
    selection[i].html('button #' + (i+1));
}

Despite all of this behavior, the jQuery object cannot be treated as a true JavaScript array. It is missing the first-class functions that we expect from a true array such as .push() or .pop(). The jQuery documentation often refers to this object as the matching set. I prefer this technology as it follows the behavior of sets and functions in abstract math—that sets are immutable and functions return new sets.

Starting a Chain

To understand the importance of this array-like behavior, we can look at the jQuery API documentation for .addClass(). This function returns a jQuery object—which contains the set of elements on which we called it. Unless the jQuery function modifies the matched set it was called against, returning a jQuery object implies that it is returning the original set. Even functions such as .first() or .last() return a set—it just contains a single object.

I typically define a chain as linking two or more jQuery functions together in a single statement as defined by the semicolon. In Part 1, I used this chaining to select elements from the scope I passed to my jQuery function:

scope.find('.button')
    .click(function(){
        alert("You Clicked The Button!");
    });

I will place any chained function on a new, indented line when one of the following conditions have been met:

  1. The function being chained has a functioned embedded as one of the arguments.
  2. The function being chained would create a line that would wrap in my editor.
  3. Another function in the chain is on a new, indented line.

I'm sure you'll notice that I did not give an explicit number of characters to a long line of code. I have used far too many tools and editors to give you a 'best' number. This number should already be something that your team is aware of in the rest of the development for a project.

Putting it Into Practice

I am a firm believer that this can, and should be taken further. Take this example, where the same set of selections need multiple event handlers:

scope.find('div.vehicle-button a').hover(
    function(){
        $(this).parent().addClass('vehicle-button-hover');
        $(this).parent().find('div.button').addClass('button-hover');
    },
    function(){
        $(this).parent().removeClass('vehicle-button-hover');
        $(this).parent().find('div.button').removeClass('button-hover');
    }
);
scope.find('div.vehicle-button a').mousedown(function(){
    $(this).parent().find('div.button').addClass('button-click');
});
scope.find('div.vehicle-button a').mouseup(function () {
        $(this).parent().find('div.button').removeClass('button-click');
});

I find the above code disorganized and inefficient. We are making multiple calls to .find() and $(this). Further, you can remove any single statement from the above and alter the behavior of the code dramatically. I can organize the above int a single, chained statement:

scope.find('div.vehicle-button a')
    .hover(
        function () {
            $(this).parent()
                .addClass('vehicle-button-hover')
                .find('div.button')
                .addClass('button-hover');
        },
        function () {
            $(this).parent()
                .removeClass('vehicle-button-hover')
                .find('div.button')
                .removeClass('button-hover');
        }
    )
    .mousedown(function () {
        $(this).parent()
            .find('div.button')
            .addClass('button-click');
    })
    .mouseup(function () {
        $(this).parent()
            .find('div.button')
            .removeClass('button-click');
    });

Each line after the chain begins indents to illustrate the depth and links of the chain. Instead of 3 statements, we have a single statement with the top line furthest to the left. If we pick any line in the code, we can quickly and easily identify what is happening and all other related events by looking up and to the left.

One thing I must mention is that while I find the above code much easier on the eyes, it is space-inefficient. Properly structured JavaScript code will always be space-inefficient. If you want space-efficient JavaScript, you had better start looking at minifiers and not trying to optimize things on your own. As most of my web development is on the .NET platform, Chirpy to minify and bundle my JavaScript files.

Keeping the Chain from Breaking

Any time we need to perform certain operations, the chain can come to a halt. Say for instance we want to increase the height of all of the vehicle buttons by 5 pixels. This is where we use .each() to keep the chain alive.

scope.find('div.vehicle-button a')
    .each(function(){
        $(this).height($(this).height() + 5);
    })
    .hover (
    ...

For the fully informed, this could have been done with .css('height', '+=5'). I just couldn't think of another example off the top of my head.

One caveat with using .each() is that you still must pay close attention to the scope you are working in—the same as in any other anonymous function that you pass into a jQuery function. This could potentially be another piece of my jQuery Best Practices guide, but won't be visited again for a while. The next chapter of my guide will be how to handle insert elements into the DOM.

Attachments:
February 20th, 2012