Advertisement

Custom Events, and the Special Events API in jQuery

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

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</p>

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!