Custom Events and Special Events API in jQuery

Custom Events, and the Special Events API in jQuery

Tutorial Details
  • Topic: jQuery
  • Difficulty: Intermediate
  • Estimated Completion Time: 30 minutes

Web pages, for the most part, are event-driven. Libraries such as jQuery have provided helper methods to make this functionality much easier to grasp. In this tutorial, we’ll look at expanding upon these methods to create your own custom namespaced events.


Events in JavaScript

Before the luxury of JavaScript libraries, if you wanted to add a simple click event to an element you needed to do the follow to support all browsers:

	var elt = document.getElementById("#myBtn");
	
	if(elt.addEventListener)
	{
		elt.addEventListener("click", function() {
			alert('button clicked');
		}); 
	} 
	else if(elt.attachEvent) 
	{
		elt.attachEvent("onclick", function() {
			alert('button clicked');
		});
	}
	else
	{
		elt.onclick = function() {
			alert('button clicked');
		};
	}	

Now JavaScript libraries come with helper methods to make event management more digestible. For example, doing the above in jQuery is much more condensed.

	$("#myBtn").click(function() {
		alert('button clicked');
	});	

Regardless of your implementation, there are three main parts to events:

  • Listener – waits or ‘listens’ for an event to fire.
  • Dispatcher – triggers the event to fire.
  • Handler – function to be executed when event is fired.

In our click event in the beginning of the tutorial, the listener is the click event waiting for the #myBtn element to be clicked. When the #myBtn element is clicked, it dispatches and will fire the handler; which in this case, is an anonymous function to display the alert() message.


Step 1: Setting up our Page

jQuery allows us to go a step further and create our own custom events. In this tutorial, we’ll be using an unordered list of a directory listing and add functionality via custom events that will collapse and expand directories. Let’s start with our basic page structure that will be used in the upcoming examples.

	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
	<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>
		<title>jQuery Custom Events</title>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		
		<style type="text/css">
			body 		{background: #fefefe; color: #111; font: 12px Arial, Helvetica, sans-serif;}
			
			#tree 	{color: #333; line-height: 14px}		
				.directory  	{list-style-image: url('images/directory.png');}
				.css  		{list-style-image: url('images/css.png');}
				.html 		{list-style-image: url('images/html.png');}
				.js 		{list-style-image: url('images/js.png');}
				.gif, 
				.png,
				.jpg 		{list-style-image: url('images/image.png');}
		</style>
		
	</head>
	<body>		
		<ul id="tree">
			<li>root/
				<ul>
					<li>index.html</li>
					<li>about.html</li>
					<li>gallery.html</li>
					<li>contact.html</li>
					<li>assets/
						<ul>
							<li>images/
								<ul>
									<li>logo.png</li>
									<li>background.jpg</li>
								</ul>
							</li>
							<li>js/
								<ul>
									<li>jquery.js</li>
									<li>myscript.js</li>
								</ul>
							</li>
							<li>css/
								<ul>
									<li>page.css</li>
									<li>typography.css</li>
								</ul>					
							</li>
						</ul>
					</li>
				</ul>
			</li>		
		</ul>
		
		<script type="text/javascript" src="http://google.com/jsapi"></script>
		<script type="text/javascript">
			google.load("jquery", "1");
			google.setOnLoadCallback(function() {
				$(function() {
					addIcons(); 

					
				});
				function addIcons()
				{
					$("#tree li").each(function() {
						if($(this).children("ul").length)
						{
							$(this).addClass("directory");
						}
						else
						{
							var txt = $(this).text();				
							var fileType = txt.substr(txt.indexOf(".") + 1);
							$(this).addClass(fileType);
						}
					});
				}
			});
		</script>
	</body>
	</html>	

Here we are creating a simple directory listing using an unordered list. We’ve included jQuery from the Google JSAPI CDN and called addIcons(), which adds images of each file and folder depending on the file extension listed. This function is purely for aesthetic purposes. It is not necessary for any of the custom event code we are about to implement. The result of this step and can be viewed here.


Step 2: .bind() and .trigger()

Before we start adding events to our directory listing example, we need to have an understanding of how .bind() and .trigger() work. We use bind() to attach an event to all matched elements that currently reside on the page. Then use .trigger() when you want to dispatch the event. Let’s take a look at a quick example.

	$("#myBtn").bind("click", function(evt) {
		alert('button clicked');
	});
	
	$("#myBtn").trigger("click");

In the code above, when the element with an id of ‘myBtn’ is clicked, an alert message will appear. Additionally, our trigger() will actually fire the click event immediately when the page loads. Just keep in mind that bind() is how you attach an event. While .trigger(), you are forcing the event to be dispatched and execute the event’s handler.


Step 3: Custom Events using .bind() and .trigger()

The method .bind() isn’t just limited to browser events, but can be used to implement your own custom events. Let’s begin by creating custom events named collapse and expand for our directory listing example.

First, let’s bind a collapse event to all directories represented in our unordered list.

	$("#tree li:parent").bind("collapse", function(evt) {

Here we find all elements that are parents and pass the event name collapse into the .bind() method. We’ve also named the first parameter evt, which represents the jQuery Event object.

	$(evt.target).children().slideUp().end().addClass("collapsed");

Now we select the target of the event and slide up all of its children. Plus, we had a CSS class collapsed to our directory element.

	}).bind("expand", function(evt) {

We are chaining events and attaching our expand event at this line.

	$(evt.target).children().slideDown().end().removeClass("collapsed");
});

Just the opposite of our collapse event handler, in the expand event handler we slide down all the children elements of the directory elements and remove the class collapsed from our target element. Putting it all together.

	$("#tree li:parent").bind("collapse", function(evt) {
		$(evt.target).children().slideUp().end().addClass("collapsed");
	}).bind("expand", function(evt) {
		$(evt.target).children().slideDown().end().removeClass("collapsed");
	});

Just this code alone won’t do anything for us because the events collapse and expand are unknown and have no idea when to be dispatched. So we add our .trigger() method when we want these events to fire.

	$("#tree li:parent").bind("collapse", function(evt) { 
		$(evt.target).children().slideUp().end().addClass("collapsed");
	}).bind("expand", function(evt) {
		$(evt.target).children().slideDown().end().removeClass("collapsed");
	})).toggle(function() { // toggle between 
		$(this).trigger("collapse");
	}, function() {
		$(this).trigger("expand");
	});

If we run this code, our directories will now toggle when clicked between firing the collapse and expand event. But, if you click a nested directory you’ll notice our events are actually firing multiple times per a click. This is because of event bubbling.


Event Capture and Bubbling

When you click an element on a page, the event travels, or is captured, from the topmost parent that has an event attached to it to the intended target. It then bubbles from the intented target back up the topmost parent.

For instance, when we click the css/ folder, our event is captured through root/, assets/, and then css/. It then bubbles css/, assets/, then to root/. Therefore, the handler is getting executed three times. We can correct this by adding a simple conditional in the handler for the intended target.

	if(evt.target == evt.currentTarget) 
	{
		(evt.target).children().slideUp().end().addClass("collapsed");
	}

This code will check each current target of the event against the intended target, or currentTarget. When we have a match, only then will the script execute the collapse event. After updating both the collapse and expand event our page will function as expected.


Event Namespacing

A namespace provides context for events. The custom events, collapse and expand, are ambiguous. Adding a namespace to a jQuery custom event is structured event name followed by the namespace. We’ll make our namespace called TreeEvent, because our events represent the actions and functionality of a tree folder structure. Once we’ve added the namespaces to our events, the code will now look like so:

	$("#tree li:parent").bind("collapse.TreeEvent", function(evt) { 
		if(evt.target == evt.currentTarget) 
		{
			$(evt.target).children().slideUp().end().addClass("collapsed");
		}
	}).bind("expand.TreeEvent", function(evt) {
		if(evt.target == evt.currentTarget)
		{
			$(evt.target).children().slideDown().end().removeClass("collapsed");
		}
	}).toggle(function() {
		$(this).trigger("collapse.TreeEvent");
	}, function() {
		$(this).trigger("expand.TreeEvent");
	});	

All we needed to change were the event names in the .bind() and .trigger() methods for both the collapse and expand events. We now have a functional example using custom namespaced events.

Note, we can easily remove events from elements by using the unbind() method.

$("#tree li:parent").unbind("collapse.TreeEvent"); // just remove the collapse event
$("#tree li:parent").unbind(".TreeEvent"); // remove all events under the TreeEvent namespace


Special Events API

Another way to setup a custom event in jQuery is to leverage the Special Events API. There isn’t much documentation on this API, but Brandom Aaron, a core contributor of jQuery, has written two excellent blog posts (http://brandonaaron.net/blog/2009/03/26/special-events and http://brandonaaron.net/blog/2009/06/4/jquery-edge-new-special-event-hooks) to help us understand the methods available. Below is a brief explanation of the methods.

  • add – similar to setup, but is called for each event being bound.
  • setup – called when the event is bound.
  • remove – similar to teardown, but is called for each event being unbound.
  • teardown – called when event is unbound.
  • handler – called when event is dispatched.

Now, let’s look at how we can combine our custom events into a Special Event that we will call toggleCollapse.

	jQuery.event.special.toggleCollapse = {
		setup: function(data, namespaces) {
			for(var i in namespaces)
			{
				if(namespaces[i] == "TreeEvent")
				{
					jQuery(this).bind('click', jQuery.event.special.toggleCollapse.TreeEvent.handler);
				}
			}						
		},
		
		teardown: function(namespaces) {
			for(var i in namespaces)
			{
				if(namespaces[i] == "TreeEvent")
				{
					jQuery(this).unbind('click', jQuery.event.special.toggleCollapse.TreeEvent.handler);
				}
			}
		},
			
		TreeEvent: {
			handler: function(event) {
				if(event.target == event.currentTarget)
				{
					var elt = jQuery(this);						
					var cssClass = "collapsed";
					if(elt.hasClass(cssClass))
					{
						elt.children().slideDown().end().removeClass(cssClass);
					}
					else
					{
						elt.children().slideUp().end().addClass(cssClass);
					}
					
					event.type = "toggleCollapse";
					jQuery.event.handle.apply(this, arguments);
				}
			}
		}
	};	
	
	$("#tree li:parent").bind("toggleCollapse.TreeEvent", function(evt) {});

Let’s take a look at it section by section.

	jQuery.event.special.toggleCollapse = {
		setup: function(data, namespaces) {
			for(var i in namespaces)
			{
				if(namespaces[i] == "TreeEvent")
				{
					jQuery(this).bind('click', jQuery.event.special.toggleCollapse.TreeEvent.handler);
				}
			}						
		},

The first line jQuery.event.special.toggleCollapse creates a new special event called toggleCollapse. We then have our setup method, which iterates over all the namespaces of this event. Once it finds TreeEvent, it binds a click event to the matched elements, which will call jQuery.event.special.toggleCollapse.TreeEvent.handler once the event is fired. Note, we are using a click event as opposed the toggle() function we were using eariler. This is because toggle() is not an event, but an interaction helper function.

	teardown: function(namespaces) {
		for(var i in namespaces)
		{
			if(namespaces[i] == "TreeEvent")
			{
				jQuery(this).unbind('click', jQuery.event.special.toggleCollapse.TreeEvent.handler);
			}
		}
	},	

Our teardown method is similar to our setup method, but instead we will unbind the click event from all matched elements.

	TreeEvent: {
		handler: function(event) {
			if(event.target == event.currentTarget)
			{
				var elt = jQuery(this);						
				var cssClass = "collapsed";
				if(elt.hasClass(cssClass))
				{
					elt.children().slideDown().end().removeClass(cssClass);
				}
				else
				{
					elt.children().slideUp().end().addClass(cssClass);
				}
				
				event.type = "toggleCollapse";
				jQuery.event.handle.apply(this, arguments);
			}
		}
	}
};	

Here we are using the TreeEvent namespace to abstract the handler. In the handler, we toggle between a collapse and expanded state depending if the matched element contains the CSS class “collapsed”. Lastly, we set the event type to our the name of our event, toggleCollapse and use the apply() method that will execute the callback argument when we bind this Special Event.

	$("#tree li:parent").bind("toggleCollapse.TreeEvent", function(evt) {});

Finally, we bind our Special Event to the directories of our directory listing. Our final result can be viewed here.


Additonal Resources

Below are a few additional resources you might find useful when working with custom events. Thanks for reading!

Tags: jQuery
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://www.nickbrowndesign.com/ Nick Brown

    Not bad. Good reminder that you can play around with more than just the built in jQuery functions.

  • http://ramblingwood.com Alec Gorge

    I agree. This was well written and useful.

  • http://designinformer.com Design Informer

    Very nice! I never thought of this before. Thanks for writing this. Very easy to follow!

  • http://www.mjama.com Mohamed Jama

    Nice tutorial, very well written and useful!

  • http://laranzjoe.blogspot.com lawrence77

    Thanks for the tutorial Phil…

    some times if we click js/ the whole assets/ will minimize…

  • http://www.liberatocreative.com Maurizio Liberato

    Very clear tut! Well done! :)

  • Max

    liked that tut! Very good style of teaching! thx!

  • http://www.quizzpot.com Crysfel

    you should change this line:

    var elt = document.getElementById(“#myBtn”);

    for this

    var elt = document.getElementById(“myBtn”);

    without the ‘#’, in the first code example.

    regards

    • http://www.aftertheone.com Matthew Booth

      I don’t understand, if myBtn is an Id why wouldn’t you want the selector to be looking for “#myBtn”?

      • http://jmlweb.es Jose Manuel

        Because he is using plain javascript in this example, not jQuery…

  • http://www.aftertheone.com Matthew Booth

    on the code
    1. $(evt.target).children().slideUp().end().addClass(“collapsed”);

    It’s worth mentioning that .end() negates the .children() traversing and returns the chaining to $(evt.target).

    I just ran into that this week in plugin and had to figure out what was going on.

    Guess I have a ways to go with my chaining techniques.

    Where do you recommend we go to find out more about event bubbling? I’m curious to find out if bubbling causes a lot of headaches with events/effects I code.

  • http://www.ecommerce-fachwissen.de Ecommerce Blog

    Thanks for this little tutorial

  • http://jhaygamba.com Jhay Gamba

    Awesome!! Thanks for sharing

  • http://www.yellowplug.es Fer

    Amazing tutorial. Thank you so much.

  • Robert Gall

    How would I go about it to making it move for double click?

    And great tutorial by the way.

  • http://bloggerzbible.blogspot.com/ Bloggerzbible

    nice demo

  • Aaron

    This helped me quite a bit!

    If I’d not read this tutorial I would have gone ahead and written my… very… custom function; which would have taken up around 30+ lines to implement. After reading this tutorial, I’d found a little “trick” in the code which allows my custom function to not only be reduced to a few lines, but it’s more precise and practical.

    The note about catching and bubbling helped too, as I’d never known what they were before now. I’ll definitely be reading more tutorials and documentation!

    Thank you for this!

    :)