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 CFinNC 2009 (Raleigh, North Carolina) with:

A jQuery Proxy To Help Create Custom Selectors

By Ben Nadel on

One of the things that makes jQuery so gosh-darn amazing is the ease with which it can be extended. One form of extension is the addition of new pseudo selectors to be used in the selector engine. I've explored this feature before; but, the other night, I was reading James Padolsey's chapter in the jQuery Cookbook, and his discussion of custom-selector creation just didn't sit right with me. Now, I'm not talking about James' explanation - he's a rather brilliant guy and I'm often jealous of his mad skills; what I'm talking about is the regular expression hoops that he had to jump through in order to make the selector binding both flexible and easy to use.

What makes custom jQuery selector creation less-than-simple is the fact that the arguments passed to the selector callback define the selected pseudo-parameters as a single string. Meaning, that if you have the following custom selector:

:foo( a, b, c )

... "a," "b," and "c," are not passed to your callback as individual arguments, but rather as a single argument - the string, " a, b, c ". This makes using those arguments a tricky, manually-intensive action.

To try and make this easier, I wanted to create a jQuery plugin that would act as a proxy to the custom-selector creation mechanism. My vision for this was that the proxy would take care of parsing the pseudo-arguments string into a much more usable arguments array. This way, the custom-selector author would be shielded from the complexities of any regular expressions that would be needed to make the effective.

I ended up creating the bindSelector() plugin, which lives in the jQuery namespace (not the fn/prototype namespace). Before I show the plugin code, however, I wanted to demonstrate how it would be used. In the following code, you'll notice that the third argument passed to the callback is not a properties array, but rather an arguments array:

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>jQuery BindSelector() Plugin</title>
  • <script type="text/javascript" src="jquery-1.3.2.js"></script>
  • <script type="text/javascript" src="jquery.bindselector.js"></script>
  • <script type="text/javascript">
  •  
  • // Bind the "foo" selector. When you use the bindSelect()
  • // plugin, the selector's parameter data is parsed into
  • // an arguments array and passed into your callback in
  • // lieu of the properties array (which is also sent as an
  • // additional argument at the end).
  • jQuery.bindSelector(
  • "foo",
  • function(
  • node,
  • index,
  • args,
  • nodes
  • ){
  •  
  • // Just output the arguments length and array.
  • $( node ).text(
  • "Args( length: " + args.length + " ) :: " +
  • "(" + args.toString() + ")"
  • );
  • }
  • );
  •  
  • // --------------------------------------------------- //
  • // --------------------------------------------------- //
  •  
  • // When the DOM is ready, test the selector.
  • jQuery(function( $ ){
  •  
  • $( "#p1:foo( Tricia )" );
  • $( "#p2:foo( Tricia, 'Smith' )" );
  • $( "#p3:foo( 'Tricia', \"Smith\" )" );
  • $( "#p4:foo( 'Tricia\\'s place' )" );
  • $( "#p5:foo( \"Tricia \\\"too sexy\\\" Smith\" )" );
  • $( "#p6:foo( Tricia's place )" );
  • $( "#p7:foo( Tricia \"too sexy\" Smith )" );
  • $( "#p8:foo( 1, '2', 3 )" );
  • $( "#p9:foo" );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • jQuery BindSelector() Plugin
  • </h1>
  •  
  • <p id="p1"></p>
  • <p id="p2"></p>
  • <p id="p3"></p>
  • <p id="p4"></p>
  • <p id="p5"></p>
  • <p id="p6"></p>
  • <p id="p7"></p>
  • <p id="p8"></p>
  • <p id="p9"></p>
  •  
  • </body>
  • </html>

Here, I am creating the custom-selector ":foo". Notice that the parameters passed to foo, as part of the selector definition, can be extremely flexible. The body of the selector callback simply outputs the arguments array as a string. When we run the above code, we get the following page output:

Args( length: 1 ) :: (Tricia)

Args( length: 2 ) :: (Tricia,Smith)

Args( length: 2 ) :: (Tricia,Smith)

Args( length: 1 ) :: (Tricia's place)

Args( length: 1 ) :: (Tricia "too sexy" Smith)

Args( length: 1 ) :: (Tricia's place)

Args( length: 1 ) :: (Tricia "too sexy" Smith)

Args( length: 3 ) :: (1,2,3)

Args( length: 0 ) :: ()

No tricky regular expressions - the pseudo-arguments are simply parsed behind the scenes and presented to the author as an easy-to-use array. From the author's perspective, the most complicated thing about this is understanding that embedded quotes must be double-escaped (once for the jQuery selector string and then once for the quoted selector argument).

While this makes things easy for the author, the code behind does have to use some regular expression parsing. Right now, it can only handle simple values - array and object literals will have to be added at a later time.

jQuery.bindSelector()

  • // Wrap the plugin in a self-executing block so we can use
  • // the $ as the jQuery reference without conflict.
  • (function( $ ){
  •  
  • // I am the parts of the regular expression pattern used to
  • // parse the selector arguments. I am breaking it out here
  • // into an array only because regular experssions are a
  • // pain in the butt to read, so this makes it ever so slightly
  • // more easy.
  • var patternParts = [
  • "(?:" ,
  • "([^\"'\\s,][^,]*)" ,
  • "|" ,
  • "(\"(?:[^\"\\\\]|\\\\.)*\")" ,
  • "|" ,
  • "('(?:[^'\\\\]|\\\\.)*')" ,
  • ")"
  • ];
  •  
  • // This is the cache in which we will store the matched
  • // selector patterns. Once a pattern is matched once, there's
  • // no need for us to parse it more than once. The cache will
  • // take the form of:
  • // matchCache[ "selectorData" : parsedTokens ]
  • var parameterCache = {};
  •  
  • // I parse the matched selector arguments (the data sent
  • // between the parenthesis in the selector) into an array
  • // of arguments that can be passed onto the given selector
  • // callback.
  • var parseArguments = function( parameterData ){
  •  
  • // Check to see if the given parameters data has already
  • // been matched. If so, we're just going to return that
  • // as parsing it again would only result in the same.
  • if (parameterData in parameterCache){
  •  
  • // Return previous parsing.
  • return( parameterCache[ parameterData ] );
  •  
  • }
  •  
  • // This is the regular expression object that will be used
  • // to parse the selector arguments.
  • var pattern = new RegExp( patternParts.join( "" ), "g" );
  •  
  • // Thse will be the collections of arguments eventually
  • // passed into the predefined callback.
  • var tokens = [];
  •  
  • // For our matches.
  • var matches = null;
  • var nonQuotedMatch = null;
  • var doubleQuotedMatch = null;
  • var singleQuotedMatch = null;
  •  
  • // Based on the regular expression that we have defined,
  • // and the captured groups, the following matches should
  • // be as such:
  • // [ 1 ] = Non-quoted argument.
  • // [ 2 ] = Double-quoted argument.
  • // [ 3 ] = Single-quoted argument.
  • // Keep looping while the pattern can be matched.
  • while (matches = pattern.exec( parameterData )){
  •  
  • // Gather the matches into variables.
  • nonQuotedMatch = matches[ 1 ];
  • doubleQuotedMatch = matches[ 2 ];
  • singleQuotedMatch = matches[ 3 ];
  •  
  • // Check to see if we have a non-quoted argument.
  • // We'll treat this as a literal and just return
  • // the matched string.
  • if (nonQuotedMatch != null){
  •  
  • // Trim the non-quoted argument and then
  • // add it to the tokens.
  • tokens.push( $.trim( nonQuotedMatch ) );
  •  
  • } else {
  •  
  • // Any other match would be a quoted string,
  • // whether double or single quoted. In either case
  • // push the evaluated string onto the tokens. We
  • // are using eval() such that any escaped quotes
  • // become real quotes.
  • tokens.push(
  • eval(
  • "(" +
  • (doubleQuotedMatch || singleQuotedMatch) +
  • ")"
  • )
  • );
  •  
  • }
  •  
  • }
  •  
  • // Now that we have parsed the parameter data, let's cache
  • // the resultant tokens so we don't need to re-parse on
  • // any subsequent uses.
  • parameterCache[ parameterData ] = tokens;
  •  
  • // Return the parsed tokens.
  • return( tokens );
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // This plugin creates a selector callback proxy that first
  • // parses the selector argument string and then passes it as
  • // an arguments array to the given callback.
  • $.bindSelector = function(
  • name,
  • callback
  • ){
  •  
  • // Bind the proxy callback for the selector.
  • $.expr[ ":" ][ name ] = function(
  • node,
  • index,
  • properties,
  • nodes
  • ){
  •  
  • // Parse the matched properties into arguments and
  • // then execute the givne callback with the given
  • // argument collection.
  • // THIS = the current DOM node (context).
  • // NODE = the DOM node being examined.
  • // INDEX = the index of the node within collection.
  • // ( props ) = arguments
  • // NODES = matched DOM node array.
  • // PROPERTIES = original selector property data.
  • return(
  • callback.call(
  • this,
  • node,
  • index,
  • parseArguments( properties[ 3 ] || "" ),
  • nodes,
  • properties
  • )
  • );
  • }
  •  
  • };
  •  
  • })( jQuery );

Because the parsing of the pseudo arguments is processing-intensive, I am caching previously-parsed arguments, keyed by the full-argument string. This way, if you are evaluating N nodes with the same selector, the pseudo arguments only get parsed the first time and the cached arguments are passed to each subsequent node evaluation. When the bind proxy executes the original callback, it passes the parsed arguments in lieu of the original properties array; it does, however, pass the original properties array as a final argument, in case the author wants to make use of it in some way.

Personally, I think the filter() method is the bees-knees for performing complex node collection trimming; but, with a by-proxy approach to pseudo-selector creation, perhaps this might make it easier for people who prefer custom selectors.




Reader Comments

I have found using custom selectors in jQuery super useful before (for instance, a case-insensitive :contains).

This goes far beyond that, and I think I will find this very useful in the future.

Does jQuery natively cache Sizzle / selector queries like your example does? If so, I am just more impressed with it every day...

@Dan,

I believe the pseudo-selectors (at least the custom ones) happen outside of Sizzle. From what I understand, Sizzle is more about finding elements in the DOM, and then the pseudo selector is more about filtering the list of "gotten" elements.

That said, they can be very powerful, even outside of Sizzle!