How To Build A Custom jQuery Selector

Posted January 13, 2009 at 5:22 PM

Tags: Javascript / DHTML

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:

 Launch code in new window » Download code as text file »

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

 Launch code in new window » Download code as text file »

  • <!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.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page





Reader Comments

Jan 13, 2009 at 5:59 PM // reply »
207 Comments

Got to stop using Alert. Alert is evil. console.log() ftw. :)


Jan 13, 2009 at 6:00 PM // reply »
207 Comments

Got to stop using Alert. Alert is evil. console.log() ftw. :)


Jan 13, 2009 at 7:04 PM // reply »
15 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.


Jan 13, 2009 at 8:25 PM // reply »
39 Comments

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.


Jan 14, 2009 at 3:15 AM // reply »
9 Comments

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


Jan 14, 2009 at 8:07 AM // reply »
6,516 Comments

@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?


Jan 14, 2009 at 8:47 AM // reply »
9 Comments

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


Jan 14, 2009 at 9:12 AM // reply »
6,516 Comments

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


Post Comment  |  Ask Ben

Recent Blog Comments
Nov 22, 2009 at 1:56 AM
Learning ColdFusion 9: Using CFQuery In CFScript Can Enable SQL Injection Attacks
Why adobe would give you script equivalent of cfquery is beyond me. I love cfquery tag because it helps me wriite clean sql, and get away from the horrible jdbc queries If I wanted to write javali ... read »
Nov 22, 2009 at 1:45 AM
Streaming Text Using ColdFusion's CFContent Tag And The Variable Attribute
The reason you would want to do this is to stream. Ack json/xml files to ria clients I used thus technique before because putting json in response stream causes debugging info to come thru As well a ... read »
Nov 21, 2009 at 6:47 PM
Hal Helms - Real World Object Oriented Development, Sarasota - Day Five
@charlie griefer, Thank you.. ... read »
Nov 21, 2009 at 5:15 PM
Using ColdFusion Structures To Remove Duplicate List Values
@Jose Galdamez, Oh heh yeah I didn't paste the whole code. I should have defined the vars -- my bad. It's fixed thou. Thanks. ... read »
Nov 21, 2009 at 4:49 PM
Styling The ColdFusion 8 WriteToBrowser CFImage Output
Great work yet again Ben! Whilst I didn't use this whole code, I copied some of your regex code for a similar problem with the lack of an alt attribute and unescaped ampersands in CFIMAGE for Railo 3 ... read »
Nov 21, 2009 at 1:13 PM
My First ColdFusion Builder Extension - Encrypting And Decrypting CFM / CFC Files
@Ben, Because I am pedantic, I just want to make sure that everyone knows there is absolutely no encryption going on. There is only encoding and obfuscation. The cfencode tool only obfuscates your C ... read »
Nov 21, 2009 at 12:28 PM
Using ColdFusion Structures To Remove Duplicate List Values
@Jody I can't seem to get your code sample to work. If you are still having problems, try this code out and see if it gets you what you wanted. <!--- Comma delimited list with various duplicates ... read »
Nov 21, 2009 at 11:03 AM
Groovy Operator Overloading Does Not Work In The ColdFusion Context
Hi Ben, Thanks for this informative post. Now I am reading ur old posts too ... read »