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

read 5 comments

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:

JavaScript:
  1. // select some divs
  2. $('div.container')
  3.   // find some spans inside those divs and add a class to them
  4.   .find('span').addClass('baby')
  5. // pop those spans off the "stack",
  6. // returning to the previous collection (div.container)
  7. .end()
  8.   // add a class to the parent of each div.container
  9.   .parent().addClass('daddy');

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):

JavaScript:
  1. // NOT recommended!
  2. (function($) {
  3.   $.fn.grandpa = function() {
  4.     return this.parents().parents();
  5.   };
  6. })(jQuery);

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:

JavaScript:
  1. // The DOM looks like this:
  2. // <div class="grandpa">
  3. //  <div class="pa">
  4. //    <div class="child son"></div>
  5. //  </div>
  6. // </div>
  7.  
  8. var elem = $('div.son').grandpa().end();
  9. $('div.son').text( elem.attr('class') );

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:

JavaScript:
  1. (function($) {
  2.   $.fn.grandma = function() {
  3.  
  4.     var els = this.parent().parent();
  5.     return this.pushStack( els.get() );
  6.   };
  7. })(jQuery);

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().

JavaScript:
  1. // The DOM looks like this:
  2. // <div class="grandma">
  3. //  <div class="ma">
  4. //    <div class="child daughter"></div>
  5. //  </div>
  6. // </div>
  7.  
  8. var elem = $('div.daughter').grandma().end();
  9. $('div.daughter').text( elem.attr('class') );

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:

JavaScript:
  1. (function($) {
  2.   $.fn.filterByData = function( type, value ) {
  3.     return this.filter(function() {
  4.       return value != null ?
  5.         $(this).data( type ) === value :
  6.         $(this).data( type ) != null;
  7.     });
  8.   };
  9. })(jQuery);

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:

JavaScript:
  1. (function($) {
  2.   $.fn.grandparent = function( parentFilter, grandFilter ) {
  3.     if ( !grandFilter ) {
  4.       grandFilter = parentFilter;
  5.       parentFilter = undefined;
  6.     }
  7.  
  8.     var els = this.parent( parentFilter ).parent( grandFilter );
  9.     return this.pushStack( els.get() );
  10.   };
  11. })(jQuery);

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.

comment feed

5 comments

  1. Any comments on .end() vs caching selectors? (eg: parent = $("#parent") )

    Best practice? Performance improvement?

  2. This is an awesome post - thanks for sharing the info on pushStack() as this can be used to help performance when writing code that does heavy dom manipulation.

    @Esteban - without digging too deep, this would be just as performant as "caching" selectors because the essentially that is what pushStack is doing. end() just pops the stack.

    Regarding best practices, I would say, don't get too carried away and have someone have to read through a nested chain with 15 end statements in it and I would say, it's a good practice to use pushStack when writing functions as the jquery team has put this method in place most likely for performance reasons and to ensure that you can easily get back to your parent elements. This allows you to traverse and manipulate the dom based on convention instead of configuration also.

    Just my 2 cents...

  3. @Dan thanks for your reply!
    After seeing this post I created a jsperf test to see if there was any difference in performance:
    http://jsperf.com/jquery-end-vs-cached-selector
    only ran it on Chrome and the seem to perform the same.
    But I agree on the point of not chaining 15 end() calls. I haven't changed any code yet to use end(), but it's really good to know it exists

  4. Awesome, I guess I can improve my current implementation by using this technique. This method is surely under utilized. Thanks for detailed explanation.

  5. Using .pushStack() is so confusing to me, this article helps a lot! Thanks for the tips, I'm gonna start applying a few of them for my new project.

2 Pings

  1. [...] Pushing it It feels like everyday there’s something new to learn about jQuery. This time, how to use jQuery’s pushStack. [...]

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

Sorry, but comments for this entry are now closed.