JavaScript Events: From the Ground Up
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:
1 |
|
2 |
var element = document.getElementById('myElement'); |
3 |
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:
1 |
|
2 |
element.onclick = function () { alert('function 1'); }; |
3 |
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.
1 |
|
2 |
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.
1 |
|
2 |
function myFunction (evt) { |
3 |
evt = evt || window.event; |
4 |
/* . . . */
|
5 |
}
|
6 |
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.
1 |
|
2 |
element.onclick = function () { |
3 |
this.style.backgroundColor = 'red'; |
4 |
//same as element.style.backgroundColor = 'red';
|
5 |
}
|
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.
1 |
|
2 |
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:
1 |
|
2 |
element.addEventListener('dblclick', logSuccessToConsole, false); |
3 |
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:
1 |
|
2 |
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.
1 |
|
2 |
<body>
|
3 |
<div id="wrapper"> |
4 |
<div id="toolbar"> |
5 |
<span id="save">Save Work</span> |
6 |
</div>
|
7 |
</div>
|
8 |
</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
.
1 |
|
2 |
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
.
1 |
|
2 |
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.
1 |
|
2 |
e.cancelBubble = true; |
3 |
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:
1 |
|
2 |
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 . . .
1 |
|
2 |
<div id="parent"> |
3 |
<span id="child">Button</span> |
4 |
</div>
|
. . . and this event handler . . .
1 |
|
2 |
var par = document.getElementById('parent'); |
3 |
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 thethis
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:
1 |
|
2 |
var addEvent = function (element, type, fn) { |
3 |
if (element.attachEvent) { |
4 |
element.attachEvent('on' + type, fn); |
5 |
}
|
6 |
else { |
7 |
element.addEventListener(type, fn, false); |
8 |
}
|
9 |
}
|
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.
1 |
|
2 |
var addEvent = function (element, type, fn) { |
3 |
if (element.attachEvent) { |
4 |
element['e'+type+fn] = fn; |
5 |
element[type+fn] = function () { element['e'+type+fn](window.event) }; |
6 |
element.attachEvent('on' + type, element[type+fn]); |
7 |
}
|
8 |
else { |
9 |
element.addEventListener(type, fn, false); |
10 |
}
|
11 |
}
|
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
.
1 |
|
2 |
var addEvent = function(el, type, fn) { |
3 |
if(el.attachEvent) { |
4 |
addEvent = function (el, type, fn) { |
5 |
el['e'+type+fn] = fn; |
6 |
el[type+fn] = function () { el['e'+type+fn](window.event); }; |
7 |
el.attachEvent('on' + type, el[type+fn]); |
8 |
}
|
9 |
}
|
10 |
else { |
11 |
addEvent = function (el, type, fn) { |
12 |
el.addEventListener(type, fn, false); |
13 |
}
|
14 |
}
|
15 |
addEvent(el, type, fn); |
16 |
}
|
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:
1 |
|
2 |
var removeEvent = function (element, type, fn) { |
3 |
if(element.detachEvent) { |
4 |
removeEvent = function (element, type, fn) { |
5 |
element.detachEvent('on' + type, el[type+fn]); |
6 |
element[type+fn] = null; |
7 |
}
|
8 |
}
|
9 |
else { |
10 |
removeEvent = function (element, type, fn) { |
11 |
element.removeEventListener(type, fn, false); |
12 |
}
|
13 |
}
|
14 |
removeEvent(element, type, fn); |
15 |
}
|
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
1 |
|
2 |
btn.addEvent('click', doThis); |
better than
1 |
|
2 |
addEvent(btn, 'click', doThis); |
We could do this by wrapping our functions in this:
1 |
|
2 |
var E = Element.prototype; |
3 |
E.addEvent = function(type, fn) { |
4 |
addEvent(this, type, fn); |
5 |
}
|
6 |
E.removeEvent = function (type, fn) { |
7 |
removeEvent(this, type, fn); |
8 |
}
|
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:
1 |
|
2 |
<ul id="nav"> |
3 |
<li id="home">home</li> |
4 |
<li id="portfolio">portfolio</li> |
5 |
<li id="about">about</li> |
6 |
<li id="contact">contact</li> |
7 |
</ul>
|
We can use event delegation to deal with clicks to each li
:
1 |
|
2 |
function navigation(e) { |
3 |
var el = e.target || e.srcElement; |
4 |
switch(el.id) { |
5 |
case 'home': |
6 |
alert('go home'); |
7 |
break; |
8 |
case 'portfolio': |
9 |
alert('check out my products'); |
10 |
break; |
11 |
case 'about': |
12 |
alert('all those disgusting details'); |
13 |
break; |
14 |
case 'contact': |
15 |
alert('I\'m flattered you\'d like to talk'); |
16 |
break; |
17 |
default: |
18 |
alert('how did you get here?'); |
19 |
}
|
20 |
}
|
21 |
var nav = document.getElementById('nav'); |
22 |
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!