Custom jQuery Events and Selector Filters

Elijah Manor | January 4th, 2010

jQuery supports a wide variety of built-in events and selectors. But despite the rich functionality that jQuery provides, you eventually come to a spot where you need an event or a selector that isn’t supported natively. The good news is that jQuery makes it easy to extend the library to support custom events and selectors.

In this article, I’ll take a look at some of the events and selectors that jQuery makes available and then move on to show how you can extend jQuery to support your custom needs. You can view, run, and edit the code samples on JS Bin (jsbin.com). Each example points to the specific URL to use.

Standard Events

In the sections that follow, I’ll cover some of the standard events you can use in jQuery. These are foundational concepts I want to be sure you know about before I dive into creating custom events.

Event Helpers

I imagine that most readers are familiar with event helpers such as blur, change, click, dblclick, error, focus, keydown, and many more. These shortcut methods make it easy to wire up handlers when one of these events occurs. Here is a quick example of attaching the click event helper to a selection and displaying a message to the console when the event fires (jsbin.com/uduwa/edit).

<script type="text/javascript">
$(function() {
   $(".clickMe").click(function(e) {
      console.log("I've been clicked!");
   });
});
</script>

Bind Method

If you want to bind multiple events to your selection, you need to use the bind method instead of the event helpers. In the following example (jsbin.com/olono3/edit), I bind the click and dblclick events to a selection and display a message to the console when the event is fired.

<script type="text/javascript">
$(function() {
   $(".clickMe").bind("click dblclick", function(e) {
      console.log("I've been clicked!");
   });
});
</script>

Live Method

Another method you need to know about when it comes to events is the live method. Using the preceding bind code snippet, let's say that you created a new DIV with the class clickMe after the DOM is loaded. Do you think the text "I've been clicked!" will be printed to the console when it is clicked? The answer is no. You have to register the event handler again, which isn't what you may have intended. If you use the live method instead of the bind method, any new dynamic DOM elements matching your criteria execute the event handler you specified. So let's try the preceding code again, but use the live method this time (https://jsbin.com/iloza/edit).

<script type="text/javascript">
$(function() {
   $(".clickMe").live("click", function(e) {
      console.log("I've been clicked!");
   });
   $("#addClickMe").click(function(e) {
      $("<p class='clickMe'>Click Me</p>").appendTo('body');
   });
});

I added a #addClickMe button that, when you click it, dynamically creates and appends a new paragraph element with the class clickMe, which happens to match the selector that I defined when registering the live event. Not only is "I've been clicked!" displayed in the console when I click on the initial paragraph, this text is also displayed when I click on any of the dynamically created paragraphs.

You might notice that I didn't provide both click and dblclick in the preceding live example, as I did with the bind example. I did this because the live method can bind only one event per call, whereas the bind method can handle multiple events.

Which One Should I Use?

If you are dealing with dynamically created DOM elements, you definitely want to use the live method, but what if that isn't the situation you face? Should you use the live or the bind technique? To answer that question, you need to think about some performance concerns. The bind method attaches your event handler during page load to the DOM element you selected. If a lot of bind methods are being set during page load, performance might be hurt. The live method, on the other hand, is registered at the document level and does most of its processing when the event has fired to figure out which event handler to call. The live method might speed up page loading, but it might also slow down performance while your events are being fired. Your choice often rests on how many DOM elements you have attached events to, how often events are fired, and how deep your DOM tree is. I would encourage you to test each technique on your site for the best performance. 

Custom Events

As you've just seen, lots of different options are available in jQuery when it comes to events. However, creating your own event can come in very handy from time to time. 

I think Rebecca Murphey says it best:

It turns out that custom events offer a whole new way of thinking about event-driven JavaScript. Instead of focusing on the element that triggers an action, custom events put the spotlight on the element being acted upon.

You can find a overview of what event-driven programming looks like in jQuery by watching a quick, five-minute video by Benson Wong entitled Event Driven Programming with jQuery Tutorial. His video provides a quick overview of using bind, live, and trigger, and then he goes into how these methods can work together when you use custom events and event-driven programming.

Even if you have not heard the term event-driven programming, you still might be familiar with the observer design pattern, which is also known as the publish and subscribe pattern. Design patterns are a set of reusable techniques that commonly occur in software development. I'll leave it to you to read further about this and many other patterns.

Before we start writing an application using a custom jQuery event, let's take a look at how to define and call a simple custom event. Instead of using the event helpers, you need to use either the bind or live method to assist you in making a custom event. So let's create our first custom event. It’s as easy as calling the bind or live method but with a custom name.

$(".clickMe").bind("quadrupleClick", function(e) {
  console.log("Wow, you really like to click!");
});

Now that we've attached a quadrupleClick event handler to the results of the .clickMe selector, how do we go about making the event handler fire? Well, you use the trigger method (jsbin.com/abiye3/edit).

var numberOfClicks = 0;
$(".clickMe").click(function() {
  numberOfClicks++;
  if (numberOfClicks >= 4) {
    $(this).trigger("quadrupleClick");
    numberOfClicks = 0;
  }
});

This code attaches an already defined click event to the .clickMe element and then records the number of times it is clicked. Once the number of clicks is four or greater, it triggers the quadrupleClick event, which fires the event handler defined previously, and the message "Wow, you really like to click" is displayed in the console.

Now let's change gears and see how you might use this technique in a more complicated scenario.

Sample *$s Application

I am going to write a small program that calculates how much a cup of coffee costs based on the extras that a customer adds to it. Here is what the no-frills UI looks like.

No frills UI

You first select the items that you want in your coffee (such as an extra espresso shot, flavored syrup, or soy milk), and then you select the type of coffee you want. After you make your selections, the total price shows up at the bottom of the page. A user is not required to select an extra feature first. Users should be able to just as easily select a coffee first and then select one of the extras.

In addition, the application has some weird rules, such as:

  • Extra shots always cost $0.25.
  • Syrup costs $0.50 extra on the weekend, $1.00 on Monday or Friday, and $0.25 any other day.
  • Soy milk costs $1.00 unless it is December, and then it’s only $0.50 extra.

Here is the HTML for the UI. Again, my focus is not on making it pretty but on the interaction between the elements and in showing how a jQuery custom event can help you create a cleaner, more extensible code base.

<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<meta charset=utf-8 />
<title>Event Oriented Programming: Old Style</title>
</head>
<body>
  <h3>Welcome to *$s</h3>
  <div id="extras">
    <input id="extraShot" type="checkbox">Extra Shot ($0.25)
    <input id="syrup" type="checkbox">Syrup (varies)
    <input id="soyMilk" type="checkbox">Soy Milk (varies)
  </div>  
  <div>
    <select id="coffee" size="5">
      <option value="2.00">Caffe Latte ($2.00)</option>
      <option value="2.50">Caffe Mocha ($2.50)</option>
      <option value="2.20">Cappuccino ($2.20)</option>
      <option value="1.45">Espresso ($1.45)</option>        
    </select>
  </div> 
  <div>
    Total Price: <strong>$<span id="totalPrice">0.00</span></strong>
  </div>
</body>
</html>

jQuery Not Using Custom Events

First, I am going to write the code in a non-event-driven way (jsbin.com/eqoci/edit), and then I'll rewrite it using event-driven programming and utilize a custom jQuery event.

$(function() {
  $('#coffee').change(function(e) {
    var updatedPrice = parseFloat($(this).val());

    if(!isNaN(updatedPrice)) {
      if($('#extraShot').is(':checked')){
        updatedPrice += extraShotPrice();
      }
    
      if($('#syrup').is(':checked')){
        updatedPrice += syrupPrice();
      }
    
      if($('#soyMilk').is(':checked')){
        updatedPrice += soyMilkPrice();
      }    
    
      $('#totalPrice').text(updatedPrice);
    }
  });
  
  $('#extras input').change(function(e) {
    $('#coffee').trigger('change');
  });
});

function extraShotPrice() {
  var price = 0.25;
  console.log('Extra Shot $0.25');
  return price;
}
 
function syrupPrice() {
  var price = 0;
  
  var day = new Date().getDay();
  switch (day) {
    case 0: case 6:
      price = 0.50;
      console.log('Syrup (Sat/Sun) $0.50');
      break;
    case 1: case 5:
      price = 1.00;
      console.log('Syrup (Mon/Fri) $1.00');
      break;
    case 2: case 3: case 4:
      price = 0.25;
      console.log('Syrup (Tue/Wed/Thu) $0.25');
      break;
  }     
  
  return price;
} 

function soyMilkPrice() {
  var price = 0;
  
  var month = new Date().getMonth();
  if (month === 11) {
    price = 0.50;
    console.log('Soy Milk (Dec) $0.50');
  } else {
    price = 1.00;
    console.log('Soy Milk (Jan/Feb/Mar/Apr/May/Jun/Jul/Sep/Nov) $1.00');
  }
  
  return price;
}

This code is fairly readable. You can see that most of the work is performed in the change event handler of the #coffee select element. First, the cost from the select option is applied and then a series of checks are made to see whether the cost of any additional shots, syrups, or soy milk should be added to the price. By the end of the event handler, the final price is applied to the #totalPrice span element and is displayed to the user.

The good things about this code are that it is fairly simple to read, you can find out pretty much all that it is doing in one event handler, and the flow is straightforward. The downside to the code is that it requires changes in multiple places if you want to add another coffee extra or modify one of the existing extra items. This violates the open closed principle, which is one of Bob Martin's S.O.L.I.D. principles. The open closed principle states that:

Software Entitles (Classes, Modules, Functions, etc.) should be open for extension, but closed for modification

Now let’s take a look using a custom jQuery event to change the coffee selection code and apply event-driven programming or the observer pattern.

jQuery Using Custom Events

I reused the extraShotPrice, syrupPrice, and soyMilkPrice methods from the previous code, so I am not going to list them again here. I’ll just show the code that I replaced (jsbin.com/asude/edit).

$(function() {
  $('#coffee').change(function(e) {
    $('#totalPrice').text($(this).val());
    $('#extras input').trigger('selected:coffee', this);
  });

  $('#extraShot').bind('selected:coffee change', function(e, caller) {
    updatePrice(extraShotPrice(), this, caller);
  });
   
  $('#syrup').bind('selected:coffee change', function(e, caller) {
    updatePrice(syrupPrice(), this, caller);
  });    
 
  $('#soyMilk').bind('selected:coffee change', function(e, caller) {
    updatePrice(soyMilkPrice(), this, caller);
  });  
});

function updatePrice(price, checkBox, caller) {
  if ($('#coffee option').is(':selected')) {
    var isChecked = $(checkBox).is(':checked');
    var currentPrice = $('#totalPrice').text();
    if (isChecked || !caller) {
      var updatedPrice = calculatePrice(currentPrice, price, isChecked);
      $('#totalPrice').text(updatedPrice);  
    }
  }
}

function calculatePrice(currentPrice, extraPrice, isSelected) {
  extraPrice = isSelected ? parseFloat(extraPrice) : (parseFloat(extraPrice) * -1);
  return parseFloat(currentPrice) + extraPrice;
}

As you can see, this code is quite different from the previous version. The change event handler off the #coffee select element doesn’t do much work compared to the first example. Really, the only thing it does is set the starting price and then trigger a custom event called selected:coffee.

The rest of the work is performed by the event handlers attached to the particular extras that are selected by the user. For example, if the user selects an extra shot and soy milk, the code first updates the base price and then triggers an event to anyone listening that a coffee order has been selected. The #extraShot and #soyMilk event handlers then respond to the event and run their code, which individually updates the base price with the price of the extra. After all the events are consumed, the final price reflects the amount the user needs to purchase the product.

This code is still fairly easy to read, but you can't go to one location any longer and see the big picture of what’s going on. As a result, debugging can be a little trickier.

Note: The updatePrice method is a little more complicated than I wanted in this example, mainly because I needed to account for the source of who triggers the event. If a user first selects a coffee (without selecting an extra), I didn't want to subtract the price of unselected extras from the base price, which is why you see me checking the caller object, which represents a new coffee being selected.

This piece of code is also very extensible. I can add another coffee extra without touching any of the existing methods. For example, if I want to add chocolate as an extra, all I need to do is add the following two methods and HTML (jsbin.com/ediye3/edit). Notice that I don’t touch any of the existing code. By coding like this, you help ensure that you don't break any of the code that you had previously.

<input id="chocolate" type="checkbox">Chocolate ($0.45)

$('#chocolate').bind('selected:coffee change', function(e, caller) {
  updatePrice(chocolatePrice(), this, caller);
}); 
 
function chocolatePrice() {
  var price = 0.45;
  console.log('Chocolate $0.45');
  return price;
}

On a side note, it’s a good idea to have unit tests wrapping your code. The jQuery unit testing framework, called QUnit, is a good start if you are interested in researching this. Adding unit tests around your code can give you confidence that you didn't break anything when you refactor code as we just did.

Standard Selector Filters

Now that we’ve tackled custom jQuery events, let's look at selector filters. From what I've seen, selector filters are one of those things that are very cool, but not a lot of people use them to their full potential.

Some commonly known selector filters are :first, :even, :odd, :eq(), :contains(), and some lesser-known ones are :header, :animated, :nth-child(), and [attribute*=].

Just as Paul Irish encourages you to learn about the lesser-known methods in his article 10 Advanced jQuery Performance Tuning Tips, you should also take time to learn about the lesser-known selector filters.

You can easily use these selector filters to narrow down your selection to the elements you are targeting in the DOM. For example, here are some examples of selectors you might use:

//Selects all the input elements (text, password, radio, etc...)
$(':input') 

//Selects all the disabled input elements
$(':disabled') 

//Selects the 4th (0 based) list item
$('li:eq(3)') 

//Selects list items that have links inside of them
$('li:has(a)')

Custom Selector Filters

Not only do you have access to a myriad of selector filters, but you can also define your own and use them to assist you in your selections.

First, let's look at how to create a really simple selector filter. Then I’ll focus on some nice custom selector filters created by developers in the community.

Selector Filter Template

The following example shows the basic template layout of a custom selector filter. Four parameters are passed into the method, but you probably won’t use many of them. I have mimicked the parameter names found in the jQuery source code. The parameters that I focus on in this article are the elem and match parameters that contain the DOM element being tested and any parameters that may have been passed to the selector filter.

/// <summary>
/// This is a jQuery Custom Selector Filter Template
/// </summary>
/// <param name="elem">Current DOM Element</param>
/// <param name="i">Current Index in Array</param>
/// <param name="match">MetaData about Selector</param>
/// <param name="array">Array of all Elements in Selection</param>
/// <returns>true to include current element or false to exclude</returns>
$.expr[":"].test = function(elem, i, match, array) {
  return true;
};

//Example usage
$(".clickMe:test").fadeIn("slow");

A Simple Custom Selector Filter

Here is a simple example of using the selector filter template to create a custom jQuery selector filter to find all the cheap coffees from the *$s application (jsbin.com/evefa/edit). Cheap is defined as $2.00 or less.

<select id="coffee" size="5" multiple="multiple">
  <option value="2.00">Caffe Latte ($2.00)</option>
  <option value="2.50">Caffe Mocha ($2.50)</option>
  <option value="2.20">Cappuccino ($2.20)</option>
  <option value="1.45">Espresso ($1.45)</option>        
</select>
$.expr[":"].cheap = function(elem, i, match, array) {
  return parseFloat($(elem).val()) <= 2.00;
};

//Example Usage
$("#coffee option:cheap").attr("selected", "selected");

The custom selector filter ends up selecting the caffe latte and espresso coffees because they are both $2.00 or less in base value.

A Simple Custom Selector Filter with Parameters

You have probably seen other selector filters that use parameters, such as the :contains(text) and :eq(index) filters. Let's tweak our simple selector filter and pass in a parameter to define what cheap means (jsbin.com/epixu/edit).

$.expr[":"].lessThanEqualTo = function(elem, i, match, array) {
  return parseFloat($(elem).val()) <= parseFloat(match[3]);
};

//Example Usage
$("#coffee option:lessThanEqualTo('2.20')").attr("selected", "selected");

As you can see, we can now pass a value to the new custom jQuery selector filter called :lessThanEqualTo and have the filter match against that value instead of hard coding $2.00, as we did previously. The parameters passed to the selector filter are stored in the third index of the match argument. If you were to examine the match argument from the previous selection, it would look like this:

$("#coffee option:lessThanEqualTo('2.20')").attr("selected", "selected");

/// <param name="match">MetaData about Selector</param>
[
  ":lessThanEqualTo('2.20')", //Selector Filter with Arguments
  "lessThanEqualTo", //Selector Filter Only
  "'", //Type of Quote Used (if any)
  "2.20" //Arguments
]

Regex Selector Filter

Now on to some interesting jQuery custom selector filters that I've seen recently. Probably the coolest one is James Padolsey's Regex Selector for jQuery. Here are some examples of using this selector filter (jsbin.com/enohu/edit):

//Select all the title attributes that end in an !
$("div:regex(title,!$)").addClass('highlight');

//Select all the text fields that have non a-ZA-Z0-9_ characters
$(":text:regex(value,[^\\w])").addClass('highlight');

There are many more examples of the types of selections you can make. Check out James Padolsey’s blog post for more information about this handy selector filter. James wrote another blog post, entitled Extending jQuery's Selector Capabilities, in which he lists many examples of custom selectors that you might find helpful.

Selector Filter for ASP.NET WebForms

Another selector filter that I think is neat is John Sheehan's Custom jQuery Selector for ASP.NET WebForms. One of the pains of ASP.NET WebForms 3.5 and earlier versions is that your HTML element IDs are mangled by INamingContainer, resulting in long and unpredictable ID values.

//Example of a mangled id attribute
<h3 id="ctl00_MainContent_ctl20_ctl00_UserLabel">Selector for ASP.NET WebForms</h3>

You can navigate your way around these altered IDs in several ways (as indicated in John's blog post), but I found John's approach of using a selector very interesting (jsbin.com/osozi/edit).

String.prototype.endsWith = function(str) {
    return (this.match(str + '$') == str)
}

jQuery.expr[":"].asp = function(a, i, m) {
  return jQuery(a).attr('id') && jQuery(a).attr('id').endsWith(m[3]);
};

//Example usage
$(":asp('UserLabel')").addClass('highlight');

One of the downsides of this selector is that it might select other DOM elements that also end with the specified string, which is the same disadvantage of using the [attribute*=value] selector filter (jsbin.com/ileta/edit).

$("[id$='UserLabel']").addClass('highlight')

I'm not sure which technique is faster, but neither of them is particularly fast because they use an implied universal selector as their first argument. You can find out more information about this in Paul Irish's jQuery Anti-Patterns presentation on slide 32.

Despite my concerns about speed, I haven't  found a good way of selecting the mangled elements gracefully and fast. I have used the [attribute*=value] selector when writing ASP.NET WebForms, and I find that John's approach has a quicker and easier syntax. I encourage you to look at John's blog post because he does a good job explaining the pros and cons of each approach.

ASP.NET WebForms 4.0 solves a lot of these problems by allowing you to control your own ID names and has numerous options for how to control your ID attributes.

Case Insensitive Contains Selector Filter

Another jQuery selector I like is from Rick Strahl's blog post Using jQuery to Search Content and Creating Custom Selector Filters. In the article he creates a selector filter called :containsNoCase. The reason he developed the selector is that the built-in :contains(text) selector is case sensitive, and Rick wanted "SQL" to match "Sql", "sql", and so on.  Here is the selector filter that he created to solve this need (jsbin.com/ajedu/edit):

$.expr[":"].containsNoCase = function(el, i, m) {
    var search = m[3];
    if (!search) return false;
    return eval("/" + search + "/i").test($(el).text());
};  

//Example Usage
$("tr:containsNoCase(jquery)").addClass("highlight");

This code searches for all table rows with the content "jQuery", "jquery", "JQuery", and so on. If the code finds a match, it adds a highlight class to the rows. Rick examines some other neat tidbits in his article, so I recommend you give it a read.

A Final Word

Events and selector filters are powerful and easy to use features of jQuery. We can be thankful that we aren't tied down to the core library implementations. jQuery is open and extensible, and developers can define and use their own events and selector filters in their applications.

Some topics from this article that you might consider for future learning are unit testing concepts such as QUnit, common design patterns to use in your projects, and the S.O.L.I.D principles of software. In addition to these, I highly recommend reading the blog posts that I linked to throughout the article. These men and woman are at the top of their field and regularly give back to the community.

 

About the Author

Elijah Manor is a Christian and a family man. He develops at appendTo as a Senior Architect providing corporate jQuery support, training, and consulting. He is an ASP.NET  MVP, ASPInsider, and specializes in ASP.NET MVC and jQuery development. He enjoys blogging about the things he learns. He is also active on Twitter and provides daily up-to-date Tech Tweets.

Find Elijah on: