Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2009) with:

How To Build A Custom jQuery Selector

Posted by Ben Nadel

The other day, I wanted to build a custom selector in jQuery. I've built a basic one before, but the one I wanted to build this time was more complex; this one would required arguments to be passed into it. I looked all around the jQuery.com website and through Google and I had a really hard time trying to piece together how a complex, custom jQuery selector should be built. After a lot of searching and much trial and error, I think I finally figured it out.

The code for a custom jQuery selector can be a simple string that gets evaluated (as in my pervious example); but, this style of programming is very hard to read and is not much fun to write. As such, the jQuery custom selector architecture that I'm interested in is that which is defined as a method call. From what I could gather, you custom jQuery selector methods are passed 4 arguments:

function( objNode, intStackIndex, arrProperties, arrNodeStack ){ ... }

objNode :: HTML DOM Element

This is a reference to the current DOM element being evaluated. This is not a jQuery version of the element but the actual DOM node.

intStackIndex :: Int

This is the zero-based index of the given node within the stack of nodes that will be evaluated with this selector call.

arrProperties :: Array

This is an array of meta data about the custom jQuery selector execution. Of this, only the fourth argument (index 3) is of any real value - it contains the string data that was passed to the jQuery selector.

arrNodeStack :: Array

This is an array of the DOM elements that are being evaluated with this selector. The inStackIndex integer above is the current DOM element's index within this array.

One of the tricky things to figure out is that the data you pass to your selector (in the jQuery command) is forwarded to your function as a single string. So, for instance, if have a selector:

:foo( 'A', 'B' )

... you don't get two different arguments passed to your custom jQuery selector function. Rather, you get the one string:

"'A', 'B'"

... passed as the fourth argument in the properties array mentioned above. Now, as this is not very useful to us programmatically, I have found that the best approach is to convert this data string into an array that we can use internally. The easiest way to do this is it modify the above data string so that it becomes the JSON (Javascript Object Notation) version of itself as an array. This JSON can then easily be evaluated resulting in a true array that mirrors the intent of our custom jQuery selector arguments:

  • var arrArguments = eval(
  • "([" + arrProperties[ 3 ] + "])"
  • );

Remember, when we evaluate JSON data, we have to add the parens around it to get it to evaluate without error.

Now that we have a handle on the core anatomy of a custom jQuery selector method, let's look at a concrete example. Below, I have a created a custom jQuery selector that selects nodes whose TITLE attribute that matches any one of the ones passed into the selector:

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>How To Build A Custom Selector In jQuery</title>
  •  
  • <script type="text/javascript" src="jquery-1.2.6.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Define the custom selector.
  • $.expr[':'].hasTitle = function(
  • objNode,
  • intStackIndex,
  • arrProperties,
  • arrNodeStack
  • ){
  • // Create an array to hold the arguments passed to
  • // the custom jquery selector. The first several
  • // data items are just meta-data about the selector.
  • // The list of arguments gets passed as a string -
  • // we are eval'ing it into an array.
  • var arrArguments = eval(
  • "([" + arrProperties[ 3 ] + "])"
  • );
  •  
  • // Create a jQuery version of this node.
  • var jThis = $( objNode );
  •  
  • // Check to see if this node has any of the given
  • // titles. If so, return true (we only need to find
  • // one of the titles to qualify).
  • for (var i = 0 ; i < arrArguments.length ; i++){
  •  
  • // Check for title equality.
  • if (jThis.attr( "title" ) == arrArguments[ i ]){
  •  
  • // We found a title match, return true to
  • // indicate that this node should be included
  • // in the returned jQuery stack.
  • return( true );
  •  
  • }
  •  
  • }
  •  
  • // If we have made it this far, then we found no
  • // match. Return false to indicate that this node
  • // did not match our selector.
  • return( false );
  • }
  •  
  •  
  •  
  • // Once the page has loaded, get the matching
  • // nodes based on title.
  •  
  • $(
  • function(){
  • var jGirls = $( "li:hasTitle( 'foo' )" );
  •  
  • // Alert girl names.
  • jGirls.each(
  • function( intI ){
  • alert( $( this ).text() );
  • }
  • );
  • }
  • );
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • How To Build A Custom Selector In jQuery
  • </h1>
  •  
  • <ul>
  • <li title="foo">Sarah</li>
  • <li title="bar">Kim</li>
  • <li title="foo">Molly</li>
  • <li title="bar">Michelle</li>
  • </ul>
  •  
  • </body>
  • </html>

In this version of the demo, we are using the jQuery command:

li:hasTitle( 'foo' )

This is going to utilize our custom jQuery selector, passing in "foo" as the list of possible titles. And, in fact, when we run this code, the Javascript alerts the following names:

  • Sarah
  • Molly

It has correctly found the two nodes whose TITLE attribute is "foo".

Now, if we modify our jQuery command to be this:

li:hasTitle( 'foo', 'bar' )

... when we run the code, the Javascript alerts the following names:

  • Sarah
  • Kim
  • Molly
  • Michelle

As you can see, it now matches all the nodes that have a TITLE attribute that is either "foo" or "bar" (which in our example is all of the LIs).

jQuery is probably the most amazing Javascript library that has ever been created. And, the fact that it allows us to author our own selectors makes it all the more powerful. I hope that this tutorial helps those looking to leverage this feature of the library.

Tweet This Fascinating post by @BenNadel - How To Build A Custom jQuery Selector Thanks my man — you rock the party that rocks the body!



Reader Comments

I appreciate that this may just be an example chosen to demonstrate the writing of a custom selector, but if not...

Why would you not just use:

$("li[title='foo']")

And for both, you could use:

$("li[title='foo'], li[title='bar']")

The attribute matching also allows you to match substrings.

Reply to this Comment

Expanding on Seb's comment, you could actually write a quick function to accept variables.

function findTitle(title) {
$("li[title='" + title + "'], li[title='" + title + "']");
}

I just typed that straight in, so I can't guarantee that it works, but I am sure that you get what I am talking about.

Reply to this Comment

An interesting approach but like it's been said this can already be done with CSS attr selectors. I realize that your extension offers a little more because it can accept multiple titles. Just a word though, I would avoid 'eval' at all costs; it's probably okay using it here but in other situation it can have unseen results.

Here's another way it could work:

$.expr[':'].hasTitle = function(elem,i,match){
return (new RegExp('^(' + match[3].replace(',','|')
.replace(/\s/g,'\\s?') + ')$')).test(elem.title);
}

This would accept titles in this format (without quotes):

$( "li:hasTitle(foo,bar)" )

Reply to this Comment

@All,

The example was insignificant; it was merely something quick I put together to both test and demonstrate the creation of custom selectors in jQuery; the main purpose of the post was the anatomy and walk through of the custom jQuery selectors.

That said, I'm sorry that I didn't have a better example on hand :) I got onto this topic because I wanted to build a custom jQuery selector that looked into the data() storage of elements, for which I don't think there are any built-in selectors. This was to be part of a much larger AJAX application.

@James,

Do not fear eval(). It is the basis for all JSON work in Javascript and without it, we could not have JSON-based AJAX. It is awesome.

I have never had any unforeseen results when using eval(); do you wish to expand on this?

Reply to this Comment

@Ben

"Do not fear eval(). It is the basis for all JSON work in Javascript and without it, we could not have JSON-based AJAX. It is awesome."

JSON and the decoding of JSON can be achieved without the use of eval. In fact, eval is one of more dangerous approaches when using JSON because it runs the JSON regardless of it's contents - i.e. it could be malicious, especially if you're using a third party API. I haven't looked into it as much as I should've but a few libraries appear to have parsing methods for JSON. (Look here for what I mean: http://svn.berlios.de/wsvn/jayrock/trunk/www/json.js)

Running JavaScript in a global context can also be achieved by injecting it in to the DOM inside a script tag; a pretty rudimentary approach but it seems to work, here's an extract from jQuery:

-------
var head = document.getElementsByTagName("head")[0] || document.documentElement, script = document.createElement("script");
script.type = "text/javascript";
if ( jQuery.browser.msie ) script.text = data;
else script.appendChild( document.createTextNode( data ) );
head.insertBefore( script, head.firstChild );
head.removeChild( script );
--------

I've been told that eval should only be used when necessary. Obviously there are situations when there is no other simple alternative but in your particular case it's not entirely necessary.

You mentioned that you're looking for a :Data selector for jQuery. I've done something like that over here: http://james.padolsey.com/javascript/extending-jquerys-selector-capabilities/ (It may not be exactly what you're after though)

Reply to this Comment

@James,

I can understand the caution against using eval() with JSON for third-party API requests; that's totally valid. But, I think that that is an outlier case. I would guess that in 99.99% of cases, people are using eval() on data that is produced and consumed within in the same application (if not the same page). In that case, there are no security concerns and really, no reason that I can think of for not using eval() given that it is very fast, native to the language, and poses no security concerns.

That said, it's good to know that there are libraries that will actually parse the JSON rather than evaluating it - I did not even know that that was an option.

The script tag to me seems the same as the eval() method, just a different route?

BTW - awesome blog. In fact, I think I may have even seen that blog post; in fact, that blog post may have been what triggered my idea. Good stuff :)

Reply to this Comment

Very interested in this, but I was wondering if you could explain something.

I plugged your code into a view that is rendered within one of our layout pages with a ton of other list items on the page and threw a console log of the arguments in the custom selector just to get a little more clarification as to what is coming through. When doing so, I saw it was running the log for every list item on the entire page. To remedy this, I threw a class of "test" on the unordered list and then changed your JavaScript a bit like so:

var jGirls = $( "ul.test li:hasTitle( 'foo' )" );

My hope was that it would only execute the custom selector on the four list items within the unordered list with a class of "test". However, it appears it still executes the custom selector against all list items on the page. The length of the above statement returns an accurate value, but I am curious as to why it still needs to rip through every list item on the view and not just those qualified by the selector. The concern is not the results since they appear to be correct, but rather the performance loss of evaluating elements that don't need to be evaluated.

Reply to this Comment

@Scott,

The reason it has to run the selector on each LI is because it relying on the pseudo selector to return a true/false as to whether to include the given element in the final results. I think what you are hoping for (from what I think you are saying) is kind of placing the cart before the horse: to have the selector *only* run on the target elements would be to *already* have the elements you are looking for.

Unless I am misunderstanding you?

Reply to this Comment

@Ben,

Well I didn't think that was the case, but it could be confusion on my part. Let me try and clarify.

What I would expect to happen based on the selector I posted above, would be for it to find a any list item within an unordered list with a class of "test" and then once it has narrowed results to this, then narrow the results down further by those that have a title of "foo".

Is it possible I am just not truly understanding how a selector works? Is the order of operation actually backwards in the selector? What I mean by this is this. Does the selector first find any list item with a title of "foo", and then within that result set, exclude those that are not contained within an unordered list with a class of "test"?

Reply to this Comment

@Scott,

After thinking about it some more, I think what you are experiencing is the fact that Sizzle (jQuery's selector engine) now performs document queries in reverse order; this is done to highly optimize the number of document queries that need to be made. In essence, it is getting all of the li:hasTitle() elements and THEN walking up the DOM tree to find ones that have the parent, ul.test. As such, ul.test does NOT do any pre-selection.

... of course, that is just my theory.

Also, thanks for bringing this up - you got me to do some more thinking about custom selector execution in general:

http://www.bennadel.com/blog/1817-jQuery-Custom-Selector-Execution-Exploration.htm

Reply to this Comment

@Kristopher,

Yes, that does the same thing as:

$( "*:customSelector" )

... But the "*" selector must get EVERY tag in the document before the selector is applied. You should try to limit the selection with a tag or class name first.

Reply to this Comment

Hi Ben
Could you please help me understand meaning of the following code
what these characters represent or do or means
/\.(jpe?g|png|bmp|gif)(\?.+)?$/.test(this.src);

from the code

jQuery('img').filter(function(){
return /\.(jpe?g|png|bmp|gif)(\?.+)?$/.test(this.src);
});

this code is from
jQuery Cookbook

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.