Traversal vs. Collection Filtering In jQuery
When it comes to jQuery selectors, I tend to think in two different modes: collection filtering and traversal filtering. By that, I mean that I see filtering as happening at two different and distinct times in the selection process. With collection filtering, jQuery filters nodes only once it has compiled them into a collection. All filtering done at that point is done in the context of the collection. With transversal filtering, on the other hand, jQuery filters nodes as it gathers them during DOM traversal. All filtering done at that point is done before a final collection has been established. This is my philosophical interpretation of what's happening - I don't truly know how this filtering is done at the technical level. That said, I have found this type of thinking to be very useful.
To demonstrate what I'm talking about, let's take a quick look at some HTML:
<table>
<tr>
<td>
<span>
<em>Hello</em>
</span>
</td>
<td>
<span>
<em>World</em>
</span>
</td>
</tr>
<tr>
<td>
<span>
<em>Hello</em>
</span>
</td>
<td>
<span>
<em>World</em>
</span>
</td>
</tr>
</table>
Given this HTML, imagine that I want to bold the EM element contained within the first SPAN of each table row (TR). Because I use the jQuery pseudo selector, ":first", so often, whenever I see the keyword "first", I tend to want to apply this filtering mechanism. As such, let's see what happens when I apply the ":first" selector to our SPAN element:
$( "tr span:first em" )
.css( "font-weight", "bold" )
;
While this might sound like a good approach, when we run this selector, we get the following output:
Hello World
Hello World
As you can see, only the first EM in the table was bolded - not the first EM within each row. The mistake that we made here was using the wrong type of selector. The pseudo selector, ":first", is a collection filter; meaning, it filters nodes in the context of a collection, not in the context of DOM traversal. As such, jQuery collected all of the SPAN elements of each row into a single collection and then filtered them down to a single node.
What we need to do is use a traversal filter - one that filters in the context of the DOM. Unfortunately, jQuery does not provide any cousin-style traversal filtering (ie. "sibling" filtering across different parents). As such, we'll need to make our selector a bit more detailed. In this case, we'll need to add a TD selector that has, attached to it, some traversal filtering:
$( "tr td:first-child span em" )
.css( "font-weight", "bold" )
;
As you can see, we've added the ":first-child" selector to the TD to contextualize the SPAN element. The pseudo selector, ":first-child", is a traversal filter that is applied in the context of the DOM. As such, the TD element is only filtered in the context of its parent TR, not in the context of the entire TD collection. In doing so, we have slightly altered the intent of our selector - making it dependent on the TD, not on the SPAN - but, we do end up with the desire output:
Hello World
Hello World
As you can see, the appropriate EM elements have been bolded. Only, it's no longer the EM contained within the first SPAN, it's the EM contained within the first TD. Like I said, we've had to slightly alter the intent of our selector; but, at least this demonstrates the difference in capabilities between collection and traversal filtering.
In most cases, it's not necessary to think too deeply about your jQuery selector choices; typically, they're so small that the difference between collection and transversal filtering becomes insignificant. But, in cases where your selectors get longer, the difference between these two modes of filtering becomes more relevant.
Want to use code from this post? Check out the license.
Reader Comments
@Ben:
The problem I have with this entry is your two different selectors have completely different meanings.
In the first example you're not grouping by table cell at all, but the second one you are. Also, there's a difference between the behavior in :first and :first-child.
The :first filter grabs the first item in the collection of elements. The :first-child filter grabs the every match of the first one found.
I think this entry would be more clear if you used the following markup:
<div>
<div>
<span>
Hello
</span>
<span>
World
</span>
</div>
<div>
<span>
Hello
</span>
<span>
World
</span>
</div>
</div>
And then used selectors that are grabbing the same elements:
$( "div span:first em" )
.css( "font-weight", "bold" )
;
$( "div span:first-child em" )
.css( "background-color", "#ffcccc" )
;
(You can view the code here: http://jsbin.com/ozeke)
The main thing is understanding how the filters actually operate.
@Dan,
Ha ha, Dan, sometimes I think we just completely miss each other on communication :) Yes, the two different selectors are completely different. I am not saying they do the same thing at all; in fact, I'm saying that they work at two different times and I even state that the intent of the second selector is not the same as the first:
In doing so, we have slightly altered the intent of our selector - making it dependent on the TD, not on the SPAN
I will try to make my language more clear in future posts.
Excellent explanation about these two concepts of jQuery. Something more that I will need to keep in mind.
hmm, I'm sure you're correct and all, but to me :first makes sense as only ever returning a single item, you can't have more than one 'first' thing in a collection, so my first instinct on looking at the example was something more like...
$('tr').each(function(){
$(this).find('span:first em').css("font-weight","bold");
});
...makes more sense to me in solving the problem as originally described, for each 'tr' we find the first spans em and bold it.
Probably not what you were trying to show though :)
@Adam,
That will definitely work. I was only trying to demonstrate different ways to think about filtering; approaching this way is certainly a good way as well.
Whilst we're skinning this cat:
$('tr').each(function(){
$('em',$(this)).first().css("fontweight","bold");
});
@Jon:
Instead of:
$('em',$(this)).first().css("fontweight","bold");
You're better off using:
$('em', this).first().css("fontweight","bold");
- or -
$(this).find('em').first().css("fontweight","bold");
There's some overhead in calling $(), so if you can minimize the usage, the better off you'll be.
I personally like the find() method--I think it's more readable.
@Dan,
I agree re: find(). It's very left-to-right readable.
I really like this 'philosophical' interpretation of the way different jQuery selectors are working.
With jQuery it's very easy to come up with a dozen different ways of achieving the same thing (as several of the comments demonstrate). The difficulty is finding the 'best' way to get the job done, i.e. what is the most efficient set of selectors to use to get to the element(s) you want?
Given the title of this article I was hoping to find some form of testing and results demonstrating which method would be the quickest in a given example.
I'm specifically interested in the performance difference between a contextualised $() selector statement vs. a chained filter method. e.g. $('.someClass', '#myEl') vs. $('#myEl').filter('.someClass')
Any ideas?