Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

JavaScript Events: From the Ground Up

by

Almost everything you do in JavaScript is supposed to begin when something happens: the user clicks a button, hovers over a menu, presses a key, you get the idea. It's pretty simple to do with libraries like jQuery, but do you know how to wire up events in raw JavaScript? Today, I'm going to teach you just that!


Welcome to JavaScript Events

Handling JavaScript events isn't rocket science. Of course, there are browser differences (what did you expect?), but once you get the hang of it and build your own helper functions to even the playing field, you'll be glad you know it. Even if you still plan to use a library for this kind of thing, it's good to know what's going on under the surface. Here we go.


Level 0 Event Handling

The most basic event handling in JavaScript is Level 0 event handling. It's pretty easy: just assign your function to the event property on the element. Observe:

 
	var element = document.getElementById('myElement'); 
	element.onclick = function () { alert('your function here'); };

Once I have the element, I set the onclick property equal to a function. Now, when element is clicked, that function will run, and we'll see an alert box.
But how did I know to use onclick? You can get a list of available events at Wikipedia. The beauty about the Level 0 method is that it's supported on all browsers widely used today. The drawback is that you can only register each event once with each element. So this will only cause one alert:

 
	element.onclick = function () { alert('function 1'); }; 
	element.onclick = function () { alert('I override function 1') };

Only the second function will run when the element is clicked, because we reassigned the onclick property. To remove an event handler, we can just set it to null.

 
	element.onclick = null;

Although it only provides basic functionality, the consistent support across all browsers makes Level 0 event handling a valid option for super-simple projects.


Full Screencast



The Event Object and This

Now that you're warming into JavaScript events, let's discuss two important facets before moving on to better techniques: the event object and the this keyword. When an event is executed, there's a lot of information about the event you might want to know: what key was pressed? what element was clicked? did the user click with the left, right, or middle button? All of this data and more is stored in an event object. For all browsers except Internet Explorer, you can get to this object by passing it as a parameter to the handling function. In IE, you can get it from the global variable window.event. It's not hard to resolve this difference.

 
	function myFunction (evt) { 
	    evt = evt || window.event; 
	    /* . . . */ 
	} 
	element.onclick = myFunction;

myFunction takes the event parameter for all good browsers, and we reassign it to window.event if evt doesn't exist. We'll look at a few of the properties of the event object a bit later.

The this keywork is important within event-handling functions; it refers to the element that received the event.

 
	element.onclick = function () { 
	    this.style.backgroundColor = 'red'; 
	    //same as element.style.backgroundColor = 'red'; 
	}

Unfortunately, in Internet Explorer, this doesn't refer to the acted-on element; it refers to the window object. That's useless, but we'll look at a way to remedy that in a bit.


Level 2 Event Handling

Level 2 event handling is the W3C specification for DOM events. It's quite simple, actually.

 
	element.addEventListener('mouseover', myFunction, false);

The method is called addEventListener. The first parameter is the type of event we want to listen for. Usually, it's the same name as the Level 0 events, without the 'on' prefix. The second parameter is either a function name or the function itself. The final parameter determines whether we capture or not; we'll discuss this in a moment.
One of the big benefits of using addEventListener is that we can now assign multiple events for a single event on a single element:

 
	element.addEventListener('dblclick', logSuccessToConsole, false); 
	element.addEventListener('dblclick', function () { console.log('this function doens\'t override the first.'); }, false);

To remove these handlers, use the function removeEventListener. So, to remove the function we added above, we do this:

 
	element.removeEventListener('dblclick', logSuccessToConsole, false);

Capturing and Bubbling

The final parameter in addEventListner and removeEventListener is a boolean that defines whether we fire the event in the capturing or bubbling stage. Here's what that means:
When you click an element, you're also clicking each one of it's ancestor elements.

 
	<body> 
	    <div id="wrapper"> 
	        <div id="toolbar"> 
	            <span id="save">Save Work</span> 
	        </div> 
	    </div> 
	</body>

If I click the span#save, I've also clicked div#toolbar, div#wrapper, and body. So if any of these elements have event listeners listening for clicks, their respective functions will be called. But what order should they be called in? That's what the last parameter decides. In the W3C spec, our click event would start at the body and move down the chain until it reaches the span#save; that's the capturing stage. Then, it goes from the span#save back up to the body; that's the bubbling stage. This is easy to remember, because bubbles float upwards. Not all events bubble; that Wikipedia chart will tell you which ones do and which ones don't.

So if the capture parameter is set to true, the event will be triggered on the way down; if it's set to false, it will be triggered on the way up.

You may have noticed that, so far, all my examples have set capturing to false, so the event is triggered in the bubbling stage. This is because Internet Explorer doesn't have a capturing phase. In IE, events only bubble.


Internet Explorer Event Model

IE has its own, proprietary way of working with events. It uses attachEvent.

 
	element.attachEvent('onclick', runThisFunction);

Since IE events only bubble, you don't need the third variable. The first two are the same as with addEventListener, with the big exception that IE requires that 'on' prefix for the event type. Of course, you can assign multiple events of one kind to an individual element with this model. To remove events, use detachEvent.

 
	element.detachEvent('onclick', runThisFunction);

Momentarily Back to Bubbling

What if your elements don't want their parents finding out about their events? It's not hard to hide. In IE, you use the cancelBubble property on the event object that gets passed to the called function. For the rest, you can use the stopPropogation method, also on the event object.

 
	e.cancelBubble = true; 
	if (e.stopPropagation) { e.stopPropagation(); }

The stopPropogation method will also stop events in their tracks during the capturing stage.


A Few Properties of the Event Object

I'd be willing to bet that most of properties on the event object will go unused, but there are a few select ones that you've find invaluable. Let's look at those few.


target

The target property is the deepest element that was clicked. In the image above, you can see that I clicked h1#button. In IE, this property is called srcElement; to get at this property, do something like this:

 
	var target = e.target || e.srcElement;

currentTarget

The currentTarget property is the element that has the event right now. In that screenshot, the currentTarget is div#wrapper. This means that I clicked h1#button, but the event object was logged to the console in the function called by the div#wrapper's event listener.

But this will throw you for a loop: IE doesn't have a currentTarget property, or anything like it. Recalling the fact that IE doesn't even let this equal the element triggering the event, this means that we have no way of getting the element that triggered the event when the is bubbling up (meaning that a descendant of the element was clicked). Confused? Maybe an example will help: if, in Internet Explorer, we have this HTML . . .

 
	<div id="parent"> 
	    <span id="child">Button</span> 
	</div>

. . . and this event handler . . .

 
	var par = document.getElementById('parent'); 
	parent.attachEvent('onclick', doSomething);

. . . then, when we click the span#child and the event bubbles up to div#parent, we have no way from inside the function doSomething to get at the element (in this case, div#parent) the event was triggered on.

Others

There are other properties on the event object that you'll find useful, depending on the case. On a keyboard event, (keyup, keydown, keypress) you'll the the keyCode and the charCode. You'll be able to tell if the user held the alt, shift, or ctrl (cmd) key while pressing/clicking. Explore the event objects of different events and see what you've got to work with!


Fixing Events

We've come across two main problems in our study of events so far.

  • IE uses a different model than the other browsers.
  • IE has no way of accessing the event's parent object; it lacks the currentTarget property, and the this object references the window object.

To solve these problems, let's build a two functions with cross-browser support, one to hook up events, and one to remove them.

We'll start with addEvent; here's round one:

 
	var addEvent = function (element, type, fn) { 
	    if (element.attachEvent) { 
	        element.attachEvent('on' + type, fn); 
	    } 
	    else { 
	        element.addEventListener(type, fn, false); 
	    } 
	}

I'm assigning an anonymous function to the variable addEvent for reasons you'll see in a minute. All we have to do is check to see if the element has the attachEvent method. If it does, we're in IE, and we'll use that method. If not, we can use addEventListener.

However, this doesn't fix the fact that we have no way to access the event's parent element in IE. To do this, we'll make the function an attribute on the element; that way, it's perceived as a method of the element, and this will equal the element. Yes, I know, brilliant, isn't it? Well, I can't take any credit: this idea comes from a blog post by John Resig.

 
	var addEvent = function (element, type, fn) { 
	    if (element.attachEvent) { 
	        element['e'+type+fn] = fn; 
	        element[type+fn] = function () { element['e'+type+fn](window.event) }; 
	        element.attachEvent('on' + type, element[type+fn]); 
	    } 
	    else { 
	        element.addEventListener(type, fn, false); 
	    } 
	}

The first extra line (element['e'+type+fn] = fn;) makes the function a method of the element, so that this will now equal the element. The second line is a function that executes the method, passing in the window.event object. This saves you the extra step of checking for the event object parameter within your function. Finally, notice that we've changed the function parameter in attachEvent method to element[type+fn].

That solves our two problems. We can now use our addEvent function in all browsers with as much of a similar experience as possible. But there's a little optimzation that we'd like to make.

Notice that each time our addEvent function is called, it checks for the attachEvent method. There's no reason why we should check for this more than once, no matter how many events we want to register. We can use a nifty trick that will allow us to re-write addEvent.

 
	var addEvent = function(el, type, fn) { 
	    if(el.attachEvent) { 
	        addEvent = function (el, type, fn) { 
	            el['e'+type+fn] = fn; 
	            el[type+fn] = function () { el['e'+type+fn](window.event); }; 
	            el.attachEvent('on' + type, el[type+fn]); 
	        } 
	    } 
	    else { 
	        addEvent = function (el, type, fn) { 
	            el.addEventListener(type, fn, false); 
	        } 
	    } 
	    addEvent(el, type, fn); 
	}

Now, if the addEvent method is found, we'll re-write addEvent to just include the IE parts; if not, we'll just include the good browser parts.
Our removeEvent function will be very similar:

 
	var removeEvent = function (element, type, fn) { 
	    if(element.detachEvent) { 
	        removeEvent = function (element, type, fn) { 
	            element.detachEvent('on' + type, el[type+fn]); 
	            element[type+fn] = null; 
	        } 
	    } 
	    else { 
	        removeEvent = function (element, type, fn) { 
	            element.removeEventListener(type, fn, false); 
	        } 
	    } 
	    removeEvent(element, type, fn); 
	}

That's it for our cross-browsers event functions. There's one thing I don't like about them, though: the functions aren't methods of the elements; instead, the element is a parameter. I know it's just a matter of style, but I like

 
	btn.addEvent('click', doThis);

better than

 
	addEvent(btn, 'click', doThis);

We could do this by wrapping our functions in this:

 
	var E = Element.prototype; 
	E.addEvent = function(type, fn) { 
	    addEvent(this, type, fn); 
	} 
	E.removeEvent = function (type, fn) { 
	    removeEvent(this, type, fn); 
	}

This will work in all modern browsers, as well as IE8; IE7 and IE6 choke on Element. It depends on your audience; take it or leave it!


Event Delegation

Now that you know all about JavaScript events, don't rush out and fill your next project with event listeners. If you've got a lot of elements responding to the same type of event (say, click), it's wiser to take advantage of bubbling. You can just add an event listener to an ancestor element, even the body, and use the target/srcElement property on the event object to figure out which deeper element was clicked. This is called Event Delegation. Here's a useless example:

Here's some HTML:

 
	<ul id="nav"> 
	    <li id="home">home</li> 
	    <li id="portfolio">portfolio</li> 
	    <li id="about">about</li> 
	    <li id="contact">contact</li> 
	</ul>

We can use event delegation to deal with clicks to each li:

 
	function navigation(e) { 
	    var el = e.target || e.srcElement; 
	    switch(el.id) { 
	        case 'home': 
	            alert('go home'); 
	            break; 
	        case 'portfolio': 
	            alert('check out my products'); 
	            break; 
	        case 'about': 
	            alert('all those disgusting details'); 
	            break; 
	        case 'contact': 
	            alert('I\'m flattered you\'d like to talk'); 
	            break; 
	        default: 
	            alert('how did you get here?'); 
	    } 
	} 
	var nav = document.getElementById('nav'); 
	addEvent(nav, 'click', navigation);

We use a switch/case statement to look at the id of the deeper clicked element. Then, we do something accordingly.


Conclusion

So now you can wield JavaScript events with the best of them. Have fun with it, and ask any questions in the comments!

Advertisement