title: You Don’t Need jQuery!
url: http://blog.garstasio.com/you-dont-need-jquery/events/
hash_url: 5313a28c96
In this fifth installment of "You Don't Need jQuery (anymore)", I'm going to talk about dealing with events in the browser without jQuery. As always, each section will cover the jQuery approach, followed by a solution using the native Web API instead. After reading this post on events, you should be confident enough to deal with events in your own project without using jQuery.
As I've mentioned (many times) before, this blog is not about bad-mouthing jQuery. jQuery is, without a doubt, ubiquitous in the world of web development. In the earlier days of web development, jQuery was required to smooth out the significant implementation differences and bugs found in various browsers when dealing with the DOM and the Web API as a whole. Also, the Web API was quite primitive at the time, in some respects, and jQuery helped to make development a bit more intuitive.
Browsers, and the Web API, have come a long way in the last several years. You can do a lot without jQuery, and I have mostly avoided jQuery in my new projects for the last several years. The jQuery approach may take less keystrokes, or look a bit more elegant in some cases. That's fine, but the point of this blog isn't to help you reduce the number of keystrokes, or write more beautiful code.
My last few posts covered selecting elements, DOM manipulation, and ajax requests. In those instances, the Web API is fairly elegant, and not much is gained by using jQuery. However, when dealing with events, the Web API is admittedly lacking in the elegance and convenience departments, even in modern browsers in some cases. That's ok, because this blog is about helping you understand the Web API so that you can avoid blindly depending on jQuery to develop your web applications. When you rely on a library to develop a large part of your application, you had better understand how the low-level API works. Otherwise you will find troubleshooting unexpected behaviors that will inevitably pop up from time to time to be quite frustrating. Keep in mind that jQuery has bugs of its own.
This post on events will primarily focus on event handling in modern browsers. We're going to define modern browsers as anything newer (and including) Internet Explorer 9. This is a commonly accepted definition, in my experience. That said, this post on events will also illustrate how jQuery was especially important when Internet Explorer 8 was commonly supported, and how you you likely will want to consider pulling in a events library, or even jQuery, to assist if you are in the unusual and unfortunate position to require support for Internet Explorer 8 or older in a new web application.
Keep in mind that the examples, descriptions, and comparisons below do not represent exhaustive references. This is all just something to help you understand what the Web API provides. Both jQuery and the Web API allow you to perform much more complex operations than the ones demonstrated here. I am merely supplying you with the basic knowledge/building blocks required to build something much more complex.
We'll start out simple here with DOM events, that is, events that are defined as part of the Document Object Model in the W3C (World Wide Web Consortium) specification.
Let's say we have an anchor tag, and we want to programmatically click it using JavaScript.
$(anchorElement).click();
Well, that was pretty easy! If we really want to do this the jQuery way (for whatever reason) we need to wrap the element so we can access jQuery's API, of course.
The above code will work in any browser available today (even IE6). jQuery certainly doesn't help us here. The code follows the same intuitive syntax if we want to trigger some other DOM events, such as focus
and blur
, or submit
on a <form>
.
We have an event, "my-custom-event" that we need to trigger. This event must bubble by default, and must be cancellable by a handler.
$('some-element').trigger('my-custom-event');
The above code will fire the custom event starting with the someElement
element. The event will bubble up the DOM by default. jQuery actually walks the event up the DOM itself, triggering any jQuery-specific event handlers and/or functions on each element that correspond to the event name (such as click()
for a "click" event).
var event = document.createEvent('Event');
event.initEvent('my-custom-event', true, true); //can bubble, and is cancellable
someElement.dispatchEvent(event);
The above code will work in all browsers. But, the createEvent
method on document
has, for the most part, been deprecated. All browsers, to my knowledge, continue to support it though.
The more "modern" approach involves using the CustomEvent
constructor:
var event = new CustomEvent('my-custom-event', {bubbles: true, cancelable: true});
someElement.dispatchEvent(event);
Unfortunately, the CustomEvent
constructor is not supported in any version of Internet Explorer to date. This will work in all other modern browsers though. If you need to support Internet Explorer (and we all do) you will need fall back to the initial example in this section. For the most part, it is probably best to continue to use createEvent
if it is supported by the browser. If newer browsers begin to remove createEvent
from their implementation of the Web API, you should be able to easily detect this and use CustomEvent
instead.
The syntax required to consume an event, DOM or custom, is very similar for modern browsers between the jQuery and native approaches. For this example, we will set up some code that will notify us when an element we're interested in has been clicked (either programmatically or via user interaction):
$(someElement).on('click', function() {
// TODO event handler logic
});
You can also make use of the click
method, which allows you to register an event handler if the first argument is a handler function:
$(someElement).click(function() {
// TODO event handler logic
});
As mentioned before, the syntax for registering an event handler using the native browser API in modern browsers (which includes IE9) is refreshingly similar to jQuery:
someElement.addEventListener('click', function() {
// TODO event handler logic
});
Note that all elements (for the most part) inherit from the Node
interface, which itself inherits from the EventTarget
interface. The first version of Internet Explorer that includes support for the addEventListener
method on the EventTarget
interface is IE9.
Whether you use jQuery or vanilla JavaScript, you must keep track of your original event handler function in order to un-register it. While jQuery provides a convenience method to remove "all" event handlers of a particular type from a specific element, it is important to understand that this will only remove any event handlers that were attached to that element via jQuery (ignoring any that may have been attached directly via addEventListener
). This is due to the fact that the Web API does not provide any way to obtain a list of registered event handlers, nor does it provide a way to blindly remove all event handlers attached to an element.
So, let's say we've previously attached the following click event handler to an element:
var myEventHandler = function(event) {
// handles the event...
}
...and we now want to remove it:
$('some-element').off('click', myEventHandler);
someElement.removeEventListener('click', myEventHandler);
The ability to modify an event generally refers to augmenting or squelching the event by one event handler before it reaches other event handlers.
Here, we want to ensure the event, caught by our event handler, will not reach any additional event handlers positioned on ancestor elements. This will stop the event from bubbling up the DOM.
$(someEl).on('some-event', function(event) {
event.stopPropagation();
});
someEl.addEventListener('some-event', function(event) {
event.stopPropagation();
});
The syntax between jQuery and the modern Web API here is almost identical.
Not only do we want to prevent this event from hitting any handlers bound to elements that are ancestors of this one, but we also want to ensure no other event handlers bound to this element are hit either. So, we want to prevent the event from hitting any further event handlers.
$(someEl).on('some-event', function(event) {
event.stopImmediatePropagation();
});
someEl.addEventListener('some-event', function(event) {
event.stopImmediatePropagation();
});
Again, the syntax between jQuery and the modern Web API here is eerily similar.
Let's say we have the following element:
<a href="http://fineuploader.com">Go to Fine Uploader</a>
...and we want to prevent a click on this anchor from opening the associated page. This will involve adding a click handler to the anchor element and instructing the browser to not execute the its native/default action.
$(someAnchor).on('click', function(event) {
event.preventDefault();
});
someAnchor.addEventListener('click', function(event) {
event.preventDefault();
});
Are you seeing a pattern here? The syntax required to deal with events is starting to look about the same between jQuery and the Web API.
Suppose you have a list filled with list items that are sensitive to mouse clicks. You could attach a click handler to each individual list item. However, this may be inefficient and slow down your page with a large number of list items. Suppose items are added to this list dynamically. Now attaching a new event handler to each new list item, as it is added, becomes less appealing.
The solution here is event delegation. That is, attach one click handler to the list. When any of the list items are clicked, the event will bubble up to its parent, the list container element. At this point, your one event handler will be hit and you can easily determine, by inspecting the event object, which list item was clicked and respond appropriately.
The markup for such a list may look like this:
<ul id="my-list">
<li>foo <button>x</button></li>
<li>bar <button>x</button></li>
<li>abc <button>x</button></li>
<li>123 <button>x</button></li>
</ul>
If the user clicks on the "x" button, the list item should be removed from the list.
jQuery will set the context of our handler to the element that initially received the click (the <button>
). Also, only <button>
elements inside this list will be examined.
$('#my-list').on('click', 'BUTTON', function() {
$(this).parent().remove();
});
We have to write a couple more lines of code without jQuery, but this still isn't exactly rocket science. The Web API always sets the context of the event handler to the element receiving the click event, which in this case is the list container element. Instead, we need to know which element was initially clicked, which is available in the target
property of the provided Event
object. We must also ensure that we only act on <button>
clicks.
document.getElementById('my-list').addEventListener('click', function(event) {
var clickedEl = event.target;
if(clickedEl.tagName === 'BUTTON') {
var listItem = clickedEl.parentNode;
listItem.parentNode.removeChild(listItem);
}
});
Remember, this isn't about elegant code, it's about exploring and understanding the Web API. After all, jQuery is just a Web API wrapper.
In this section, I'll show how to handle various keyboard events. I'll also show how to identify which specific key was pressed by the user.
First, we should take a moment to be sure we understand the difference between the three different types of general keyboard events:
keydown
: Key has been pressed but not yet released. No default action has been performed yet.keypress
: Key has been pressed and a character has registered. This event will fire continuously if the key is held down.keyup
: Pressed key has been released.Let's say we are building a web application and want to make an interactive tutorial that we've build accessible via an intuitive keyboard shortcut. Anywhere in the application, our users should be able to pull up our help widget via the Ctrl
-H
key combination.
$(document).keydown(function(event) {
if (event.ctrlKey && event.which === 72) {
// open help widget
}
});
document.addEventListener('keydown', function(event) {
if (event.ctrlKey && event.which === 72) {
// open help widget
}
});
Nothing is obviously gained, in this instance, with jQuery. Even the syntax is almost identical between the Web API and jQuery.
Registering for other keyboard events follows a similar pattern:
$(someElement).keypress(function(event) {
// ...
});
$(someElement).keyup(function(event) {
// ...
});
someElement.addEventListener('keypress', function(event) {
// ...
});
someElement.addEventListener('keyup', function(event) {
// ...
});
For more information about keyboard event properties and browser compatibility among the properties of this event, have a look at the KeyboardEvent
document on Mozilla Developer Network.
There are a number of mouse events provided by the Web API, such as "mousedown", "mouseenter", and "mouseover" (to name a few). It isn't particularly interesting to show how to register for and handle these events in jQuery and the Web API. Much like keyboard events, the syntax between the two is almost identical.
Instead, I'm going to focus on one special event that is part of jQuery's API. Of course, the goal here is to create your own code by using the plain 'ole Web API sans jQuery. I'll show you how to do that too.
hover
eventjQuery provides a way to notify you when a mouse pointer has hovered over a specific element, and then again when the mouse pointer leaves this element. For example:
$('some-element').hover(
function hoverIn() {
// mouse is hovering over this element
},
function hoverOut() {
// mouse was hovering over this element, but no longer is
}
);
We can do the same thing with the Web API pretty easily:
someEl.addEventListener('mouseover', function() {
// mouse is hovering over this element
});
someEl.addEventListener('mouseout', function() {
// mouse was hovering over this element, but no longer is
});
As I mentioned earlier, mouse events are fairly straightforward to handler with the Web API. For more information on mouse events, have a look at the MouseEvent
interface documentation on the Mozilla Developer Network.
When talking about "load" events in the browser, we may be trying to answer any one of the following questions:
This event should be fired after:
$(window).load(function() {
// page is fully rendered
})
window.addEventListener('load', function() {
// page is fully rendered
});
This event should fire after all markup has been placed on the page.
$(document).ready(function() {
// markup is on the page
});
Note that you can also achieve the same thing in jQuery this way:
$(function() {
// markup is on the page
});
document.addEventListener('DOMContentLoaded', function() {
// markup is on the page
});
Note that you will likely want to ensure your script that registers the DOMContentLoaded
event handler is placed before any stylesheet <link>
tags, since loading these stylsheets will block any script execution and prolong the DOMContentLoaded
event until the defined stylesheets have loaded.
In addition to window
, load events are associated with a number of elements, such as <img>
, <link>
, and <script>
. The most common use of this event outside of window
is to determine when a specific image has loaded.
$('img').load(function() {
// image has successfully loaded
});
You can also determine if the image has failed to load:
$('img').error(function() {
// image has failed to load
});
img.addEventListener('load', function() {
// image has successfully loaded
});
And if the image should fail to load?
img.addEventListener('error', function() {
// image has failed to load
});
As we've seen many times before, the syntax between jQuery and the Web API here for modern browsers is strikingly similar.
This is where I talk about a time when jQuery was indeed a required library for web applications. Back when it was common to support IE8 or something even older. Back then, the Web/DOM API was a bit of a mess in some instances. This was especially true when dealing with events. Below, I'm going to briefly discuss some of the ways you can deal with events using vanilla JavaScript in older browsers. I'm going to limit this to IE7 and IE8. IE6 is mostly dead at this point anyway. Though many, if not all, of these examples should apply to IE6 as well.
someElement.attachEvent('onclick', function() {
// TODO event handler logic
});
You'll notice two distinct differences between this and the modern browser approach.
attachEvent
instead of addEventListener
.Maybe you're wondering how you can easily and programmatically use the correct event handling method, based on the browser's capabilities. For most of us developing apps exclusively for modern browsers, this isn't a concern. But if, for some reason, you must target ancient browsers, such as IE8 (or older), you can use the following code to register for an event in any browser:
function registerHandler(target, type, callback) {
var listenerMethod = target.addEventListener || target.attachEvent,
eventName = target.addEventListener ? type : 'on' + type;
listenerMethod(eventName, callback);
}
// example use
registerHandler(someElement, 'click', function() {
// TODO event handler logic
});
One more thing: if you want to remove an event handler in IE8 and older, you must use detachEvent
instead of removeEventListener
. So, a cross-browser way to remove an event listener may look like this:
function unregisterHandler(target, type, callback) {
var removeMethod = target.removeEventListener || target.detachEvent,
eventName = target.removeEventListener ? type : 'on' + type;
removeMethod(eventName, callback);
}
// example use
unregisterHandler(someElement, 'click', someEventHandlerFunction);
Older versions of IE have some serious change-event deficiencies. Here are the two big ones that come to mind:
It's important to know that the 2nd issue above was also reproducible when using jQuery with IE7/8 for quite a long time. As far as I can tell, current versions of jQuery (~1.10+) do properly address this issue though.
To solve issue #1, you must attach a change handler directly to any form field that you'd like to monitor. Event delegation is not possible. For issue #2, you're best bet may be to attach a click handler to radio/checkbox fields instead of relying on the change event.
First off, in IE8 and older, the Event
object is not passed directly to a registered event handler (remember, you must use attachEvent
). Instead, the Event
is set as a property on the window
. So, if you are are writing an event handler that may be executed in modern or ancient browsers, you'll need something like this to cover both cases:
function myEventHandler(event) {
var actualEvent = event || window.event;
// handle actualEvent
}
Also, some properties of the Event
object instance are a bit different in older browsers. For example, while the target of the event in modern browsers can be found by checking the target
property of the Event
instance, IE8 and older contain a different property for this element, named srcElement
. So, now your cross-browser event handler looks like this:
function myEventHandler(event) {
var actualEvent = event || window.event,
actualTarget = actualEvent.target || actualEvent.srcElement
// handle actualEvent & actualTarget
}
In terms of controlling your events, the stopPropagation
method is not available on an Event
object instance in IE8 and older. If you want to stop an event from bubbling, you must instead set the cancelBubble
property on the event. A cross-browser solution would look like this:
function myEventHandler(event) {
var actualEvent = event || window.event;
if (actualEvent.stopPropgation) {
actualEvent.stopPropagation();
}
else {
actualEvent.cancelBubble = true;
}
}
IE8 and older also do not have a stopImmediatePropagation
method. There isn't much to be done about this. I don't see lack of this method as a big loss, as use of stopImmediatePropagation
seems like a code smell to me since the behavior of this call is completely dependent on the order that multiple event handlers are attached to the element in question.
While load
events on the window
seem to function fairly well in ancient browsers, the DOMContentLoaded
event was not supported by Internet Explorer until version 9. For older versions, the solution is a bit of a kludge. If DOMContentLoaded
is really very important to you, and you must support IE8 and older, consider pulling in the tiny contentloaded.js script.
Internet Explorer 8 supports the readystatechange event, which can be used to detect when the DOM is ready. In earlier versions of Internet Explorer, this state can be detected by repeatedly trying to execute document.documentElement.doScroll("left");, as this snippet will throw an error until the DOM is ready.
Of course, there are other gotchas in the context of event handling when dealing with ancient browsers. I've decided to outline a few of the more common ones above. If I forgot to mention something earth-shattering, please let me know in the comments.
You likely don't need a library to help you deal with events in modern browsers. But, if you really want some bells & whistles:
For me: I'll talk about various utility functions and tasks that are commonly needed when developing most web applications. This will be more of a (but not entirely) JavaScript-as-a-language focus. By contrast, most of my previous posts have focused almost exclusively on the Web/DOM API.
For you: if I've left out any important event-related topics, please let me know in the comments so I can update this post.