Whenever people ask me about the most powerful things in JavaScript and the DOM, I quickly arrive at events. The reason is that events in browsers are incredibly useful. Furthermore, decoupling functionality from events is a powerful idea, which is why Node.js became such a hot topic.
Today, let’s get back to the basics of events and get you in the mood to start playing with them, beyond applying click handlers to everything or breaking the Web with <a href="javascript:void(0)">
links or messing up our HTML with onclick="foo()"
inline handlers (I explained in detail in 2005 why these are bad ideas).
Note: This article uses plain JavaScript and not any libraries. A lot of what we’ll talk about here is easier to achieve in jQuery, YUI or Dojo, but understanding the basics is important because you will find yourself in situations where you cannot use a library but should still be able to deliver an amazing solution.
Disclaimer: The event syntax we’ll be using here is addEventListener(), as defined in the “DOM Level 3 Events� specification, which works in all browsers in use now except for Internet Explorer below version 9. A lot of the things we’ll show can be achieved with jQuery, though, which also supports legacy browsers. Come to think of it, one simple addEventListener()
on DOMContentLoaded
is a great way to make sure your script does not run on legacy browsers. This is a good thing. If we want the Web to evolve, we need to stop giving complex and demanding code to old browsers. If you build your solutions the right way, then IE 6 would not need any JavaScript to display a workable, albeit simpler, solution. Think of your product as an escalator: if your JavaScript does not execute, the website should still be usable as stairs.
Before we get into the details of events and how to use them, check out a few demos that use scroll events in a clever way to achieve pretty sweet results:
- In its search for a designer, Wealthfront Engineering uses scrolling and shifting content along the Z axis. This was a big part of the Beercamp 2011 website. Wealthfront blogged in detail about how it achieved this.
- Stroll.js takes a slightly similar approach, showing how lovely transitions can be when the user scrolls a list.
- jQuery Scroll Path is a plugin to move content along a path when the user scrolls the page.
All of this is based on event handling and reading out what the browser gives us. Now, let’s look at repeating the basics of that.
Basics: What Is An Event?
var log = document.getElementById('log'), i = null, out = []; for (var i in window) { if ( /^on/.test(i)) { out[out.length] = i; } } log.innerHTML = out.join(', ');
In my case, running Firefox, I get this:
onmouseenter, onmouseleave, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onmessage, onoffline, ononline, onpopstate, onpagehide, onpageshow, onresize, onunload, ondevicemotion, ondeviceorientation, onabort, onblur, oncanplay, oncanplaythrough, onchange, onclick, oncontextmenu, ondblclick, ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onerror, onfocus, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmousemove, onmouseout, onmouseover, onmouseup, onmozfullscreenchange, onmozfullscreenerror, onpause, onplay, onplaying, onprogress, onratechange, onreset, onscroll, onseeked, onseeking, onselect, onshow, onstalled, onsubmit, onsuspend, ontimeupdate, onvolumechange, onwaiting, oncopy, oncut, onpaste, onbeforescriptexecute, onafterscriptexecute
That is a lot to play with, and the way to do that is by using addEventListener()
:
element.addEventListener(event, handler, useCapture);
For example:
var a = document.querySelector('a'); // grab the first link in the document a.addEventListener('click', ajaxloader, false);
The element
is the element that we apply the handler to; as in, “Hey you, link! Make sure you tell me when something happens to you.� The ajaxloader()
function is the event listener; as in, “Hey you! Just stand there and keep your ears and eyes peeled in case something happens to the link.� Setting the useCapture
to false
means that we are content to capture the event on bubbling, rather than the capturing phase. This is a long and arduous topic, well explained on Dev.Opera. Let’s just say that by setting the useCapture
to false
, you will be fine in 99.7434% of cases (a rough approximation). The parameter is actually optional in all browsers but Opera.
Now, the event handler function gets an object as a parameter from the event, which is full of awesome properties that we can play with. If you try out my example, you’ll see what the following code does:
var log = document.getElementById('log'), i = 0, out = ''; document.addEventListener('click', logeventinfo, false); document.addEventListener('keypress', logeventinfo, false); function logeventinfo (ev) { log.innerHTML = ''; out = '<ul>'; for (var i in ev) { if (typeof ev[i] === 'function' || i === i.toUpperCase()) { continue; } out += '<li><span>'+i+'</span>: '+ev[i]+'</li>'; } log.innerHTML += out + '</ul>'; }
You can assign several event handlers to the same event, or the same handler to various events (as shown in this demo).
The ev
is what we get back from the event. And (again, in my case, in Firefox) a lot of interesting things are in it:
originalTarget: [object HTMLHtmlElement] type: click target: [object HTMLHtmlElement] currentTarget: [object HTMLDocument] eventPhase: 3 bubbles: true cancelable: true timeStamp: 574553210 defaultPrevented: false which: 1 rangeParent: [object Text] rangeOffset: 23 pageX: 182 pageY: 111 isChar: false screenX: 1016 screenY: 572 clientX: 182 clientY: 111 ctrlKey: false shiftKey: false altKey: false metaKey: false button: 0 relatedTarget: null mozPressure: 0 mozInputSource: 1 view: [object Window] detail: 1 layerX: 182 layerY: 111 cancelBubble: false explicitOriginalTarget: [object HTMLHtmlElement] isTrusted: true originalTarget: [object HTMLHeadingElement] type: click target: [object HTMLHeadingElement] currentTarget: [object HTMLDocument] eventPhase: 3 bubbles: true cancelable: true timeStamp: 574554192 defaultPrevented: false which: 1 rangeParent: [object Text] rangeOffset: 0 pageX: 1 pageY: 18 isChar: false screenX: 835 screenY: 479 clientX: 1 clientY: 18 ctrlKey: false shiftKey: false altKey: false metaKey: false button: 0 relatedTarget: null mozPressure: 0 mozInputSource: 1 view: [object Window] detail: 1 layerX: 1 layerY: 18 cancelBubble: false explicitOriginalTarget: [object Text] isTrusted: true
It also differs from event to event. Try clicking the demo and pressing keys, and you will see that you get different results. You can also refer to the full list of standard event
properties.
The Last Of The Basics: Preventing Execution And Getting The Target
Two more things are important when it comes to events in the browser: we have to stop the browser from carrying out its default action for the event, and we have to find out which element the event fired on. The former is achieved with the ev.preventDefault()
method, and the latter is stored in ev.target
.
Say you want to know that a link has been clicked, but you don’t want the browser to follow it because you have a great idea of what to do with that event instead. You can do this by subscribing to the click event of the link, and you can stop the browser from following it by calling preventDefault()
. Here is the HTML:
<a class="prevent" href="http://smashingmagazine.com">Smashing, my dear!</a> <a class="normal" href="http://smashingmagazine.com">Smashing, my dear!</a>
And the JavaScript:
var normal = document.querySelector('.normal'), prevent = document.querySelector('.prevent'); prevent.addEventListener('click', function(ev) { alert('fabulous, really!'); ev.preventDefault(); }, false); normal.addEventListener('click', function(ev) { alert('fabulous, really!'); }, false);
Note: document.querySelector()
is the standard way to get an element in the DOM. It is what the $()
method in jQuery does. You can read the W3C’s specification for it and get some explanatory code snippets on the Mozilla Developer Network (MDN).
If you now click the link, you will get an alert. And when you hit the “OK� button, nothing more happens; the browser does not go to http://smashingmagazine.com
. Without the preventDefault()
, the browser will show the alert and follow the link. Try it out.
The normal way to access the element that was clicked or hovered over or that had a key pressed is to use the this
keyword in the handler. This is short and sweet, but it’s actually limiting because addEventListener()
gives us something better: the event target. It could also be confusing because this
might already be bound to something else, so using ev.currentTarget
as noted in the specification is a safer bet.
Event Delegation: It Rocks. Use It!
Using the target
property of the event object, you can find out which element the event occurred on.
Events happen by going down the whole document tree to the element that you interacted with and back up to the main window. This means that if you add an event handler to an element, you will get all of the child elements for free. All you need to do is test the event target and respond accordingly. See my example of a list:
<ul id="resources"> <li><a href="http://developer.mozilla.org">MDN</a></li> <li><a href="http://html5doctor.com">HTML5 Doctor</a></li> <li><a href="http://html5rocks.com">HTML5 Rocks</a></li> <li><a href="http://beta.theexpressiveweb.com/">Expressive Web</a></li> <li><a href="http://creativeJS.com/">CreativeJS</a></li> </ul>
Hover your mouse over the list in this example and you will see that one event handler is enough to get the links, the list item and the list itself. All you need to do is compare the tagName
of the event target to what you want to have.
var resources = document.querySelector('#resources'), log = document.querySelector('#log'); resources.addEventListener('mouseover', showtarget, false); function showtarget(ev) { var target = ev.target; if (target.tagName === 'A') { log.innerHTML = 'A link, with the href:' + target.href; } if (target.tagName === 'LI') { log.innerHTML = 'A list item'; } if (target.tagName === 'UL') { log.innerHTML = 'The list itself'; } }
This means you can save a lot of event handlers — each of which is expensive to the browser. Instead of applying an event handler to each link and responding that way — as most people would do in jQuery with $('a').click(...)
(although jQuery’s on
is OK) — you can assign a single event handler to the list itself and check which element was just clicked.
The main benefit of this is that you are independent of the HTML. If you add more links at a later stage, there is no need to assign new handlers; the event handler will know automatically that there is a new link to do things with.
Events For Detection, CSS Transitions For Smoothness
If you remember the list of properties earlier in this article, there is a lot of things we can use. In the past, we used events for simple hover effects, which now have been replaced with effects using the :hover
and :focus
CSS selectors. Some things, however, cannot be done with CSS yet; for example, finding the mouse’s position. With an event listener, this is pretty simple. First, we define an element to position, like a ball. The HTML:
<div class="plot"></div>
And the CSS:
.plot { position:absolute; background:rgb(175,50,50); width: 20px; height: 20px; border-radius: 20px; display: block; top:0; left:0; }
We then assign a click handler to the document and position the ball at PageX
and pageY
. Notice that we need to subtract half the width of the ball in order to center it on the mouse pointer:
var plot = document.querySelector('.plot'), offset = plot.offsetWidth / 2; document.addEventListener('click', function(ev) { plot.style.left = (ev.pageX - offset) + 'px'; plot.style.top = (ev.pageY - offset) + 'px'; }, false);
Clicking anywhere on the screen will now move the ball there. However, it’s not smooth. If you enable the checkbox in the demo, you will see that the ball moves smoothly. We could animate this with a library, but browsers can do better these days. All we need to do is add a transition to the CSS, and then the browser will move the ball smoothly from one position to another. To achieve this, we define a new class named smooth
and apply it to the plot when the checkbox in the document is clicked. The CSS:
.smooth { -webkit-transition: 0.5s; -moz-transition: 0.5s; -ms-transition: 0.5s; -o-transition: 0.5s; transition: 0.5s; }
The JavaScript:
var cb = document.querySelector('input[type=checkbox]'); cb.addEventListener('click', function(ev) { plot.classList.toggle('smooth'); }, false);
The interplay between CSS and JavaScript events has always been powerful, but it got even better in newer browsers. As you might have guessed, CSS transitions and animations have their own events.
How Long Was A Key Pressed?
As you might have seen in the list of available events earlier, browsers also give us a chance to respond to keyboard entry and tell us when the user has pressed a key. Sadly, though, key handling in a browser is hard to do properly, as Jan Wolter explains in detail. However, as a simple example, let’s look how we can measure in milliseconds how long a user has pressed a button. See this keytime demo for an example. Press a key, and you will see the output field grow while the key is down. Once you release the key, you’ll see the number of milliseconds that you pressed it. The code is not hard at all:
var resources = document.querySelector('#resources'), log = document.querySelector('#log'), time = 0; document.addEventListener('keydown', keydown, false); document.addEventListener('keyup', keyup, false); function keydown(ev) { if (time === 0) { time = ev.timeStamp; log.classList.add('animate'); } } function keyup(ev) { if (time !== 0) { log.innerHTML = ev.timeStamp - time; time = 0; log.classList.remove('animate'); } }
We define the elements we want and set the time
to 0
. We then apply two event handlers to the document, one on keydown
and one on keyup
.
In the keydown
handler, we check whether time
is 0
, and if it is, we set time
to the timeStamp
of the event. We assign a CSS class to the output element, which starts a CSS animation (see the CSS for how that is done).
The keyup
handler checks whether time
is still 0
(as keydown
gets fired continuously while the key is pressed), and it calculates the difference in the time stamps if it isn’t. We set time
back to 0
and remove the class to stop the animation.
Working With CSS Transitions (And Animations)
CSS transitions fire a single event that you can listen for in JavaScript called transitionend
. The event object then has two properties: propertyName
, which contains the property that was transitioned, and elapsedTime
, which tells you how long it took.
Check out the demo to see it in action. The code is simple enough. Here is the CSS:
.plot { background:rgb(175,50,50); width: 20px; height: 20px; border-radius: 20px; display: block; -webkit-transition: 0.5s; -moz-transition: 0.5s; -ms-transition: 0.5s; -o-transition: 0.5s; transition: 0.5s; } .plot:hover { width: 50px; height: 50px; border-radius: 100px; background: blue; }
And the JavaScript:
plot.addEventListener('transitionend', function(ev) { log.innerHTML += ev.propertyName + ':' + ev.elapsedTime + 's '; }, false);
This, however, works only in Firefox right now because Chrome, Safari and Opera have vendor-prefixed events instead. As David Calhoun’s gist shows, you need to detect what the browser supports and define the event’s name that way.
CSS animation events work the same way, but you have three events instead of one: animationstart
, animationend
and animationiteration
. MDN has a demo of it.
Speed, Distance And Angle
Detecting events happening is one thing. If you want to do something with them that is a beautiful and engaging, then you need to go further and put some math into it. So, let’s have a go at using a few mouse handlers to calculate the angle, distance and speed of movement when a user drags an element across the screen. Check out the demo first.
var plot = document.querySelector('.plot'), log = document.querySelector('output'), offset = plot.offsetWidth / 2, pressed = false, start = 0, x = 0, y = 0, end = 0, ex = 0, ey = 0, mx = 0, my = 0, duration = 0, dist = 0, angle = 0; document.addEventListener('mousedown', onmousedown, false); document.addEventListener('mouseup', onmouseup, false); document.addEventListener('mousemove', onmousemove, false); function onmousedown(ev) { if (start === 0 && x === 0 && y === 0) { start = ev.timeStamp; x = ev.clientX; y = ev.clientY; moveplot(x, y); pressed = true; } } function onmouseup(ev) { end = ev.timeStamp; duration = end - start; ex = ev.clientX; ey = ev.clientY; mx = ex - x; my = ey - y; dist = Math.sqrt(mx * mx + my * my); start = x = y = 0; pressed = false; angle = Math.atan2( my, mx ) * 180 / Math.PI; log.innerHTML = '<strong>' + (dist>>0) +'</strong> pixels in <strong>'+ duration +'</strong> ms ( <strong>' + twofloat(dist/duration) +'</strong> pixels/ms)'+ ' at <strong>' + twofloat(angle) + '</strong> degrees'; } function onmousemove (ev) { if (pressed) { moveplot(ev.pageX, ev.pageY); } } function twofloat(val) { return Math.round((val*100))/100; } function moveplot(x, y) { plot.style.left = (x - offset) + 'px'; plot.style.top = (y - offset) + 'px'; }
OK, I admit: quite a lot is going on here. But it is not as hard as it looks. For both onmousedown
and onmouseup
, we read the mouse’s position with clientX
and clientY
and the timeStamp
of the event. Mouse events have time stamps that tell you when they happened. When the mouse moves, all we check is whether the mouse button has been pressed (via a boolean set in the mousedown
handler) and move the plot with the mouse.
The rest is geometry — good old Pythagoras, to be precise. We get the speed of the movement by checking the number of pixels traveled in the time difference between mousedown
and mouseup
.
We get the number of pixels traveled as the square root of the sum of the squares of the difference between x and y at the start and end of the movement. And we get the angle by calculating the arctangent of the triangle. All of this is covered in “A Quick Look Into the Math of Animations With JavaScript�; or you can play with the following JSFiddle example:
Media Events
Both video and audio fire a lot of events that we can tap into. The most interesting are the time events that tell you how long a song or movie has been playing. A nice little demo to look at is the MGM-inspired dinosaur animation on MDN; I recorded a six-minute screencast explaining how it is done.
If you want to see a demo of all the events in action, the JPlayer team has a great demo page showing media events.
Input Options
Traditionally, browsers gave us mouse and keyboard interaction. Nowadays, this is not enough because we use hardware that offers more to us. Device orientation, for example, allows you to respond to the tilting of a phone or tablet; touch events are a big thing on mobiles and tablets; the Gamepad API allows us to read out game controllers in browsers; postMessage allows us to send messages across domains and browser windows; pageVisibility allows us to react to users switching to another tab. We can even detect when the history object of the window has been manipulated. Check the list of events in the window object to find some more gems that might not be quite ready but should be available soon for us to dig into.
Whatever comes next in browser support, you can be sure that events will be fired and that you will be able to listen to them. The method works and actually rocks.
Go Out And Play
And that is it. Events are not hard; in most cases, you just need to subscribe to them and check what comes back as the event object to see what you can do with it. Of course, a lot of browser hacking is still needed at times, but I for one find incredible the number of ways we can interact with our users and see what they are doing. If you want to get really creative with this, stop thinking about the use cases we have now and get down into the nitty gritty of what distances, angles, speed and input can mean to an interface. If you think about it, playing Angry Birds to the largest degree means detecting the start and end of a touch event and detecting the power and direction that the bird should take off in. So, what is stopping you from creating something very interactive and cool?
Image source of picture on front page.
(al)
© Christian Heilmann for Smashing Magazine, 2012.