Jump to main content
Menu

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.
    • this keyword, 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 onclick event 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 via window.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 onclick becomes click)
    • 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)
  • 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 theButton that you wanted to execute the function validateData when 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 support addEventListener(). 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 onclick event handler. Such a straightforward approach is not possible.

  • Developers who want to pass multiple parameters, or change what this represents, can leverage the bind() 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 to addEventListener:

    let theRef = validateData.bind(theButton, paramA, paramB, paramC);
    theButton.addEventListener('click', theRef, false);
    

    Just be sure to keep theRef available, or you will not be able to remove the listener.

    Functions also support apply() and call() methods, but those execute immediately so they are not a good match for event listening.

    Like bind() they give control over what this references and can accept multiple values or even an array or an array-like object for parameters, in the case of apply().

Bubbling & Capturing Phases

  • There are three phases that occur in this sequence when an event fires:
    1. Capturing
    2. At Target
    3. 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 window or document, 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.
  • 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 true means that the function executes in the capture phase.
    • A value of false means that the function executes in the bubbling phase.
  • 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 true will occur before those set to false, 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, and unload. In an odd twist, the submit, reset, select, and change events (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 href attribute is loaded when the anchor is clicked
    • forms - the value of their action attribute is called when the form is submitted
  • 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 target property 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, or window, 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 body that is a substantial amount of DOM traversal, plus you may end up firing events accidentally on elements between the target node and body.
    • Poor fit for all events / element node combinations: The change and submit event handlers do not work on document or window.
  • 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 focus and blur events, because they do not bubble. As a result, their container element node never fires their event.
    • The workaround for focus and blur is to assign those events to the container element node as part of the capture phase (i.e., true for the third parameter), which gets them to work.
    • Or just use the focusin and focusout events that bubble up.

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 type property reflects the event listener that was triggered (e.g., mouseover, focus, etc.).
  • When using keyup and keydown event listeners, the event object has long had a keyCode property that reflects what character was pressed (ranging from 65 for 'a' to 90 for 'z').
  • However, keyCode has been deprecated and its replacement is the key property. You can learn more about the key property. In practice, you may need to switch between these as you develop for a wide variety of browsers.
  • The altKey, ctrlKey, and shiftKey properties of the event object are true if those keys were pressed when the event fired; otherwise they are false. 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-standard detachEvent().
  • 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.