Event Listeners
Non-Standard Event Handling
- The earliest versions of the DOM, before it was standardized, used dot notation to assign event handlers. These are referred to as DOM0, and the standardized versions are DOM1, DOM2, DOM3, etc.
- You will see this approach in existing code and you need to be able to explain why there are better approaches to follow.
- An example of this approach is:
elementNodeRef.onclick = someFunction;
- Benefits of this approach:
- Worked reliably cross-browser and cross-platform.
thiskeyword, when used in the function called, correctly references the element node that had the event handler assigned.
- Disadvantages of this approach:
- Not part of a standard, which means that the people who create browsers have no reference material when creating their implementation. The odds of code breaking to some degree are much higher.
- Only one event handler of a given type (e.g.,
onclick,onfocus,onblur,onkeydown) could be assigned to that element node. - If you assigned two separate
onclickevent handlers to a button, the one read last would overwrite the one read earlier. - No easy way to do event delegation.
- The limitation of only one event handler of a given type on a given element becomes a problem with
window.onload, especially if you are calling multiple external scripts that you did not author and/or that all rely on setting things up viawindow.onload. Collisions are bound to occur and only the last one read is used. The others are ignored. - The other case of DOM0 event assignment is through HTML attributes such as
onclick,onblur, etc. Those are also to be avoided. Beyond the disadvantages cited, they also intermingle structure (HTML) and behavior (JavaScript).
Event Listeners
- The standardized DOM replaces the event handler concept with a different approach called event listening.
- The
addEventListener()method is the standards-based method for associating element nodes with events and functions. addEventListener()has three parameters passed:- The first parameter is the type of event, with 'on' removed (thus
onclickbecomesclick) - The second parameter is the function to trigger when the event occurs
- The third parameter is a boolean controlling when the event fires (the bubbling and capturing phases section provides more details)
- The first parameter is the type of event, with 'on' removed (thus
- As time passed, the third parameter changed to an options object that provided even more flexibility in the event listener behavior. Supportive browsers fork their behavior based on the third parameter being a boolean primitive or an object.
- If you had a button element node referenced by the variable
theButtonthat you wanted to execute the functionvalidateDatawhen clicked, the code would be:theButton.addEventListener('click', validateData, false); - The function (in this example it is named
validateData) is automatically passed a single parameter referred to as the event object. The properties of that object give us a lot of information about what just happened in the DOM. - We omit the
()after the function name, because we do not want it executing immediately. - You may encounter old code that uses an
attachEvent()method. That is a non-standard method used in Internet Explorer 8 and below, because those browsers did not supportaddEventListener(). Microsoft was slow to get aboard the standards bandwagon. We will not consider that method further. - One advantage is that multiple functions can be assigned to the same event listener for a given element node:
theButton.addEventListener('click', validateData, false); theButton.addEventListener('click', calculateResults, false); theButton.addEventListener('click', showResults, false); - If you accidentally repeat yourself and assign the same function twice to the same element node via the same event (that is, you duplicate the same line of code twice), modern browsers will not execute the code twice because a named function can only be assigned to an element node once for a given event. Anonymous functions do not have this restriction (or this safeguard, depending on your perspective).
- There are also some disadvantages:
- We cannot always guarantee that the functions will execute in the sequence we wrote them in our code. As you write your code try to guard against rigid sequencing and dependencies across functions. We do have control in some situations, but not all.
- It is not possible to determine what event listeners have been assigned to an element node, which impacts your ability to remove them (because removing requires knowing the function name as well as the event handler and activation phase; anonymous functions cannot be removed at all).
In event handling you could simply indicate:
someElementNode.onclick = null;
And with that, you were guaranteed to have removed the
onclickevent handler. Such a straightforward approach is not possible.
- Developers who want to pass multiple parameters, or change what
thisrepresents, can leverage thebind()method that is available to Function objects. Some sample code:// first parameter for bind() determines what 'this' represents inside the function theButton.addEventListener('click', validateData.bind(theButton, paramA, paramB, paramC), false); // the parameter setting this is not mentioned // the event object (evt in this example) is passed after all the parameters // specified in bind() so be sure to give it a name function valideData(paramA, paramB, paramC, evt) { }The concern with
bind()is that the event listener can only be removed if you assign the function to a variable, and pass that toaddEventListener:let theRef = validateData.bind(theButton, paramA, paramB, paramC); theButton.addEventListener('click', theRef, false);Just be sure to keep
theRefavailable, or you will not be able to remove the listener.Functions also support
apply()andcall()methods, but those execute immediately so they are not a good match for event listening.Like
bind()they give control over whatthisreferences and can accept multiple values or even an array or an array-like object for parameters, in the case ofapply().
Bubbling & Capturing Phases
- There are three phases that occur in this sequence when an event fires:
- Capturing
- At Target
- Bubbling
- Imagine a code setup where you have a div holding an anchor; the parent element of the div is body. The anchor and the div have 'click' event listeners assigned; they call different functions.
- When you click on the anchor the capturing phase begins and starts with the
windowordocument, proceeds down to the body element, then down to the div, and then finally reaches the anchor. - Once it has reached the anchor it is at target.
- Then the bubbling phase begins and the div is encountered again as the activation works from the anchor outwards / upwards.
- When you click on the anchor the capturing phase begins and starts with the
- The function you associated with that event occurs in either the capture phase or the bubbling phase.
- The third parameter passed to
addEventListener()determines which of those two phases is used:- A value of
truemeans that the function executes in the capture phase. - A value of
falsemeans that the function executes in the bubbling phase.
- A value of
- If I wanted the div to fire its event during capture and the anchor to fire its event during bubbling, the code is:
theAnchor.addEventListener('click', someFunction, false); theDiv.addEventListener('click', someOtherFunction, true); - This also gives us some control over the sequencing of functions, because those set to
truewill occur before those set tofalse, as long as the element nodes involved are nested and the event involves all the nodes. Two listeners on the same element node triggering in the same phase will still fire in a random sequence, however (we cannot know ahead of time which will go first of the two). - It is also very important to note that not all events will bubble. Some of those that do not bubble are:
- focus
- blur
- load
- unload
- mouseenter
- mouseleave
- Of those events listed, some do not bubble because only some elements support
focus,blur,load, andunload. In an odd twist, thesubmit,reset,select, andchangeevents (all related to form elements) do bubble, but not every element supports them (and ideally would just ignore them). - The real value of this is in event delegation.
Stopping Propagation
If you need to stop the bubbling process you can call the stopPropagation() method of the event object.
Stopping Propagation Example
See the Pen Stopping Propagation by Jason Withrow (@jwithrow) on CodePen.
Considerations with the Stopping Propagation Example
- Mousing over just the list item (tabbing is omitted because you cannot tab to list items unless you start setting tabindexes on those elements) causes its event to fire. The background color of the list item changes.
- Mousing over or tabbing to the anchor inside the list item causes its event to fire, after which bubbling stops and the list item event never fires. In this case the list item marker changes from square to circle (or vice versa) and the list item never highlights, because the event stopped propagating and never reached the list item element.
Stopping Default Behavior
- Some HTML elements have default behaviors. The most commonly encountered ones are:
- anchors - the value of their
hrefattribute is loaded when the anchor is clicked - forms - the value of their
actionattribute is called when the form is submitted
- anchors - the value of their
- The event object has a
preventDefault()method that you can call. For example, your code has determined that there is invalid data in a form submission so you stop the submit default behavior.
Stopping Default Behavior Example
To the previous example we add a 'click' event to the anchors, which calls a method that triggers the preventDefault() method on the event object. Clicking on those anchors will no longer attempt to load the file referenced in their href attributes.
See the Pen Stopping Default Behaviors by Jason Withrow (@jwithrow) on CodePen.
Event Delegation
- Event delegation refers to assigning an event listener to an outer element node and having the function that is called impact an inner (nested) element node.
- The event object has a
targetproperty that references the innermost node of the outer element node that had the listener assigned. - The advantages of this approach are significant:
- If you insert new element nodes within the container element they immediately gain the functionality of the outer element node; they do not need to have events assigned to them specifically.
- Memory usage is reduced because there are fewer events being assigned.
- It is best to use event delegation if you have multiple element nodes inside the container that need events to occur for them. If you just have a single element node here and a single element node there that need events assigned, you don't need to use event delegation.
- You may encounter code that assigns events to
body,document, orwindow, because everything should eventually bubble up to them. I advise against this in most cases because of:- Performance concerns and unintended consequences: If there are a lot of element nodes between what you want to target and
bodythat is a substantial amount of DOM traversal, plus you may end up firing events accidentally on elements between the target node andbody. - Poor fit for all events / element node combinations: The
changeandsubmitevent handlers do not work ondocumentorwindow.
- Performance concerns and unintended consequences: If there are a lot of element nodes between what you want to target and
- As you may have guessed, we will need to have our code also accommodate tracking down the specific element node you want to impact, since it may not be the innermost one.
- There will also be cases where you cannot use event delegation to achieve your goals. Preventing default form behavior, for example, requires that the event object come from the form element in question, not some other element node holding the form.
- We also encounter a special situation with the
focusandblurevents, because they do not bubble. As a result, their container element node never fires their event.- The workaround for
focusandbluris to assign those events to the container element node as part of the capture phase (i.e.,truefor the third parameter), which gets them to work. - Or just use the
focusinandfocusoutevents that bubble up.
- The workaround for
Event Delegation Example
Event delegation allows us to assign the event listeners to the containing element, rather than to each nested element.
See the Pen Event Delegation by Jason Withrow (@jwithrow) on CodePen.
Other Event Object Properties and Methods
- The
typeproperty reflects the event listener that was triggered (e.g.,mouseover,focus, etc.). - When using
keyupandkeydownevent listeners, the event object has long had akeyCodeproperty that reflects what character was pressed (ranging from 65 for 'a' to 90 for 'z'). - However,
keyCodehas been deprecated and its replacement is thekeyproperty. You can learn more about thekeyproperty. In practice, you may need to switch between these as you develop for a wide variety of browsers. - The
altKey,ctrlKey, andshiftKeyproperties of the event object aretrueif those keys were pressed when the event fired; otherwise they arefalse. Those properties are not tied to keyboard-specific events (the user could be holding down Shift and click something, for example). - The event object also contains mouse coordinates if the event itself was mouse-related, although some cross-browser differences exist.
- And there are many, many more. MDN provides a run-down of all the event object properties and methods, including ones specific to certain events.
Removing Listeners
- In order to remove an event listener it must be a named function / method and you must perfectly match the parameters you used to create it (including event type and whether bubble or capture was used, or perhaps an options object). Usage of
bind()also derails removing the listener, unless the function details are passed as a reference during both creation and removal. - Anonymous functions / methods cannot be removed.
- The standards-based method is
removeEventListener(), and Internet Explorer 8 and below had the non-standarddetachEvent(). - An example:
theButton.removeEventListener('click', validateData, false); - While the browser will garbage collect as best it can, there is also value in having our code clean up after itself.
- There are also cases where you may need to remove listeners, based on user selections or preferences.