Creating a Custom Selector in jQuery

For me, one of the best, yet under-utilised features of jQuery is the custom selector.

Custom selectors are another way to extend jQuery for your own means, and to hive off worker functionality that needn’t clutter your code.

The Old Way

To demonstrate this, I’m going to use a simple example for identifying elements with empty value attributes. Consider the scenario where you want a bit of client-side script to alert you if there are any empty textboxes upon form submission:

var i = 0;
$("input:text").each(function() {
    if ($(this).val() === "") {
        i++;
    }
});

if (i > 0) {
    alert("Not all fields filled out");
}

Now, at face-value then this is a quite valid way of doing things. However, for me, this is a worker task – a task that in theory can be hived away elsewhere so as not to clutter your code. We can of course, make it into a function:

function HasEmpty(selector)
    var i = 0;
    $("input:text").each(function() {
        if ($(this).val() === "") {
            i++;
        }
    });
    return (i > 0);
}

But this really isn’t all that flexible. Do you want to return the count of items? Or just a Boolean indicating if any are empty? And it’s not really all that jQuery-like.

The New Way

So, how can we make this a little more elegant? By using custom selectors, of course. In fact, we will just be reusing a language feature of jQuery that jQuery itself uses.

Have you ever wondered how jQuery implements the CSS Pseudo-Class Selectors such as :first-child or :hidden, etc? Well, under the hood, these are all really just custom selectors built into the jQuery framework. This goes for all of the 30+ Pseudo-Class selectors in jQuery.

To do this, we make use of the little known property jQuery.expr[':'] in conjunction with jQuery.extend like so:

$.extend($.expr[":"], {selectorName:[anonymous function]});

The interesting part of this is [anonymous function]. This function will be called for every element against which this selector runs (much in the same way that .each works).

If the function returns true” then the current element will be included in the result set. Conversely, a return value of false  false” will exclude it. This function is only really useful if we can reference each object upon which it works. We can use this by using the basic definition of this function (there is an extended one which we will discuss later). This accepts a DOM object representing the current element as a parameter.

When referring to the jQuery core, we see that it adopts the convention of naming the parameter elem:

enabled: function(elem){
    return elem.disabled === false && elem.type !== "hidden";
},

disabled: function(elem){
    return elem.disabled === true;
},

Who are we to argue? So now, our Custom Selector template becomes:

 $.extend($.expr[":"], {
    selectorName:function(elem){
        return [boolean expression];
    }
});

So, returning to our original example of wanting to search for all elements with an empty val attribute, our code takes on the following form.

$.extend($.expr[':'], {
    valueEmpty: function(elem) {
        var $elem= $(elem);
        return ($elem.val() === "") && ($elem.attr("type") === "text");
    }
});

I’ve added a check to ensure the DOM element is a text box, and I’ve cached the jQuery cast of elem to avoid multiple calls into the library. Really, the implementation is secondary to the discussion here. The important thing to note is that it will return a Boolean. Calling out new code is trivial, and as we would expect:

alert($(":valueEmpty").length);

Although, I always advocate combining low-performance selectors (of which this is one) with higher performance ones (more on this later):

alert($("input:valueEmpty").length);

Parameterised Custom Selectors

You’ll notice that some Pseudo-Class selectors accept a parameter, for example :has(selector). So how do we get some of that good stuff? We are going to build a lengthBetween selector that will filter out items whose length is greater than two specified parameters, in this case, four and eight. The calling code will look like:

alert($("input:lengthBetween(4,8)").length);

The anonymous function we specified for our original custom selector actually has four parameters. We only used the first, elem. The following is the full definition:

$.extend($.expr[":"], {selectorName:function(elem, i, match, array){
    return [boolean expression];
});

So what are all of these parameters?

  • elem we are already familiar with - it is the current DOM element of the iteration stack
  • i is the index of elem upon the stack
  • match is an array that contains all information about the custom selector
  • array contains all the elements in the stack over which we are iterating

We are interested in the match array parameter, as this is where we will retrieve parameter information. The match array stores four pieces of information. So, given the the selector call:

alert($("input:lengthBetween(4,8)").length);
  • match[0] – contains the full pseudo-class selector call. In this example :lengthBetween(4,8)
  • match[1] – contains the selector name only. In this example lengthBetween
  • match[2] – denotes which, if any, type of quotes are used in the parameter expression. i.e. single (‘)  or double (“). In this example it will be empty.
  • match[3] – gives us the parameters, i.e. what is contained in the brackets. In this example 4,8

So, if we just want the parameters, then match[3] is the way forward. This is a simple string value and it’s really up to you how you handle it. You can String.Split() it to get multiple parameters, and you can consider validating the parameters somehow. Again, we are drifting away somewhat from the scope of this discussion, so I’ll refer to the jQuery core once more:

gt: function(elem, i, match){
    return i > match[3] - 0;
},

Here, a single parameter is expected and assumed. No validation attempt is made. So, returning to our :lengthBetween selector, the implementation might look something like:

$.extend($.expr[":"], {
    lengthBetween: function(elem, i, match) {
        var params = match[3].split(",");  //split our parameter string by commas
        var elemLen = $(elem).val().length;   //cache current element length 
        return ((elemLen >= params[0] - 0) && (elemLen <= params[1] - 0));  //perform our check
    }
});

So there we have it - A custom selector that will filter out values whose length is between one and three characters.

Speed and Efficiency

I have always encouraged developers to consider the speed and efficiency of their basic selectors. Although it is not an exact science (relative speeds can differ between browsers, and selector combinations can be unpredictable), I have always advocated the following rule of thumb when it comes to using your selectors:

  1. An #ID selector is quicker than element selector is quicker than .class selector.
  2. Always try to prefix .class selectors with an element selector
  3. #ID selectors almost always can be used in isolation.

For further reading, Hugo Vidal Teixeira has provided a nice analysis of jQuery selector performance.

So where do pseudo-class and custom selectors fit into this?

An #ID and element selector map directly to the core JavaScript functions .getElementById() and .getElementsByTagName().

Class, Pseudo-Class and Custom selectors all require a scan of the DOM to filter out elements, which is inherently slow. So, treat custom selectors like class Class and Pseudo-Class selectors when it comes to performance.

Resources / Further Reading

Some useful links for further reading and other custom selectors.

And Finally…

What of jQuery.expr[':']? The eagle eyed among you might have questioned as to why we should be constrained to just overriding the colon? In my opinion, this is straying into dangerous territory, however it is possible given the versatility of the Sizzle selector engine.

You can read more in this article, which documents how to override the ‘<’ symbol.

2 thoughts on “Creating a Custom Selector in jQuery

  1. Pingback: jQuery Standards (6) – Selectors « James Wiseman

Leave a Reply