A place to cache linked articles (think custom and personal wayback machine)
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.md 50KB

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.

  1. Sending Native (DOM) Events
  2. Sending Custom Events
  3. Listening For Events
  4. Removing Event Handlers
  5. Modifying Events
  6. Event Delegation
  7. Keyboard Events
  8. Mouse Events
  9. Browser Load Events
  10. Ancient Browser Support
  11. Libraries to Consider
  12. Next in this Series

Sending Native (DOM) Events

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.

jQuery

$(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.

Web API

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>.

Sending Custom Events

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.

jQuery

$('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).

Web API

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.

Listening For Events

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):

jQuery

$(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
});

Web API

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.

Removing Event Handlers

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:

jQuery

$('some-element').off('click', myEventHandler);

Web API

someElement.removeEventListener('click', myEventHandler);

Modifying Events

The ability to modify an event generally refers to augmenting or squelching the event by one event handler before it reaches other event handlers.

Preventing the event from bubbling further up the DOM

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.

jQuery

$(someEl).on('some-event', function(event) {
    event.stopPropagation();
});

Web API

someEl.addEventListener('some-event', function(event) {
    event.stopPropagation();
});

The syntax between jQuery and the modern Web API here is almost identical.

Preventing the event from hitting any additional handlers attached to the current element

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.

jQuery

$(someEl).on('some-event', function(event) {
    event.stopImmediatePropagation();
});

Web API

someEl.addEventListener('some-event', function(event) {
    event.stopImmediatePropagation();
});

Again, the syntax between jQuery and the modern Web API here is eerily similar.

Preventing the event from triggering an action defined by the browser

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.

jQuery

$(someAnchor).on('click', function(event) {
    event.preventDefault();
});

Web API

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.

Event Delegation

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

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();
});

Web API

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.

Keyboard Events

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:

  1. keydown: Key has been pressed but not yet released. No default action has been performed yet.
  2. keypress: Key has been pressed and a character has registered. This event will fire continuously if the key is held down.
  3. 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.

jQuery

$(document).keydown(function(event) {
    if (event.ctrlKey && event.which === 72) {
        // open help widget
    }
});

Web API

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:

jQuery

$(someElement).keypress(function(event) {
    // ...
});

$(someElement).keyup(function(event) {
    // ...
});

Web API

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.

Mouse Events

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.

jQuery's hover event

jQuery 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.

Browser Load Events

When talking about "load" events in the browser, we may be trying to answer any one of the following questions:

When have all elements on the page fully loaded and rendered w/ applied styles?

This event should be fired after:

  • all markup has been placed on the page
  • all stylesheets have been loaded
  • all images have loaded
  • all iframes have fully loaded

jQuery

$(window).load(function() {
    // page is fully rendered
})

Web API

window.addEventListener('load', function() {
    // page is fully rendered
});

When has all static markup been placed on the page?

This event should fire after all markup has been placed on the page.

jQuery

$(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
});

Web API

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.

When has a particular element on the page fully 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.

jQuery

$('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
});

Web API

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.

Ancient Browser Support

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.

Listening For Events

someElement.attachEvent('onclick', function() {
    // TODO event handler logic
});

You'll notice two distinct differences between this and the modern browser approach.

  1. We must rely on attachEvent instead of addEventListener.
  2. The event name must include a prefix of "on".

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);

Form Field Change Events

Older versions of IE have some serious change-event deficiencies. Here are the two big ones that come to mind:

  1. Change events do not bubble.
  2. Checkboxes and radio buttons may not trigger a change event at all.

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.

The Event Object

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.

Browser Load Events

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.

Mozilla Developer Network provides a great explanation of the logic required to mimic the behavior of the DOMContentLoaded event in ancient browsers:

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.

Libraries to Consider

You likely don't need a library to help you deal with events in modern browsers. But, if you really want some bells & whistles:

  • Consider using bean for dealing with various event-related tasks. Bean was written by one of the original developers of Bootstrap.
  • Mousetrap is a nifty little library that makes it simple and fun to handle various keyboard shortcuts.
  • PubSubJS is a small library that, as the readme states, focuses on topic-based publish/subscribe support.

Next

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.