Using jQuery’s .pushStack() for reusable DOM traversing methods

The .pushStack() method has been in jQuery since before version 1.0, but it hasn't received a whole lot of attention outside of core developers and plugin authors. While its usefulness may not be immediately apparent, it can come in really handy in some situations, so I'd like to take a quick look at what it does, how it works, and how we can use it.

pushStack Basics

At its most basic level, the .pushStack() method accepts an array of DOM elements and "pushes" it onto a "stack" so that later calls to methods like .end() and .andSelf() behave correctly. (Side note: As of jQuery 1.4.2, you can pass in a jQuery object instead of an array, but that isn't documented and jQuery itself always uses an array, so that's what we'll stick to here.)

Internally, jQuery uses .pushStack() to keep track of the previous jQuery collections as you chain traversing methods such as .parents() and .filter(). This lets us traverse through the DOM, do some stuff, "back up" to previous collections within the same chain using .end(), and then do something else. Here is a somewhat contrived example:

[js] // select some divs $('div.container') // find some spans inside those divs and add a class to them .find('span').addClass('baby') // pop those spans off the "stack", // returning to the previous collection (div.container) .end() // add a class to the parent of each div.container .parent().addClass('daddy'); [/js]

Because .find() returns the result of a .pushStack() call to keep track of the previous collection (as does .parent()), we can use .end() in the above example to return to the container divs.

Using pushStack for Fun and Profit

So, this is great for jQuery, but what can .pushStack() do for me and my code? Well, it can help me write specialized DOM traversal plugins that act just like jQuery's own traversal methods. In other words, I can stop chaining the same sets of traversal methods together and instead write a reusable function that still works with with .end() and all that. For example, let's say I often have a need to find an element's grandparent. While I could write $('#myElement').parent().parent() every time, it might be nice to just be able to write $('#myElement').grandparent() instead. A naïve way to write a grandparent plugin would look like this (changing the method name to "grandpa" for this example):

[js] // NOT recommended! (function($) { $.fn.grandpa = function() { return this.parents().parents(); }; })(jQuery); [/js]

The problem here is that two new jQuery object instances are added to the stack. So, let's see what happens when we use it:

[js] // The DOM looks like this: //
//
//
//
//
var elem = $('div.son').grandpa().end(); $('div.son').text( elem.attr('class') ); [/js]

Without seeing the plugin, we would expect to see "child son" inserted into <div class="son">, but "pa" is inserted instead. Each .parent() call in the plugin adds to the stack, so using .end() only pops the second one off.

If we use .pushStack() instead, however, we can achieve the expected behavior:

[js] (function($) { $.fn.grandma = function() { var els = this.parent().parent(); return this.pushStack( els.get() ); }; })(jQuery); [/js]

Within a plugin function, one that is a method of $.fn, the this keyword refers to the jQuery object; therefore, the els variable refers to a jQuery object, as well. To convert it to an array, we use jQuery's .get() method, and we pass that array to .pushStack(). Let's see if .grandma() works any better than .grandpa().

[js] // The DOM looks like this: //
//
//
//
//
var elem = $('div.daughter').grandma().end(); $('div.daughter').text( elem.attr('class') ); [/js]

Here, "child daughter" is inserted, which means that .end() works as expected, changing the jQuery collection from the result of .grandma() to the result of $('div.daughter'). So, we've just successfully written a DOM traversal plugin, albeit a very simple one.

The Simplest DOM Traversal Methods

If the plugin only uses one DOM traversal method, then .pushStack() isn't really necessary. The HTML5 data filter plugin written by Elijah Manor illustrates this point nicely:

[js] (function($) { $.fn.filterByData = function( type, value ) { return this.filter(function() { return value != null ? $(this).data( type ) === value : $(this).data( type ) != null; }); }; })(jQuery); [/js]

Only one new jQuery collection is added to the stack, via .filter(), so using .end() simply pops that one off, and our job is done.

Filtering grandparents

For the sake of completeness, it would be nice for this DOM traversal plugin to allow optional "filtering" of the parent and grandparent elements. After all, jQuery's .parent() and .parents() allow filtering. For example, if I were to write $('div.child').parent('.daddy'), the jQuery collection would only contain an element if div.child had a parent element and if that parent had a class of "daddy."

There are plenty of reasonable ways one could include the filters, but for my purposes I'm going to have a .grandparent() method optionally accept two arguments. If only one argument is provided, it will filter the grandparent element only; if two are provided, the first will filter the parent and the second will filter the grandparent. Here is the full plugin plugin:

[js] (function($) { $.fn.grandparent = function( parentFilter, grandFilter ) { if ( !grandFilter ) { grandFilter = parentFilter; parentFilter = undefined; } var els = this.parent( parentFilter ).parent( grandFilter ); return this.pushStack( els.get() ); }; })(jQuery); [/js]

Finally, we have a nice .grandparent() plugin that adheres to the contract set by other jQuery DOM traversal methods—one that works with both filters and the .end() method. Here is what it could look like in use.



Responsive Menu
Add more content here...