JavaScript Events

# JavaScript Events

# What are events?

Events are actions or occurrences that happen in the browser, often triggered by users interacting with a web page, or by the browser itself. Events allow us to execute code in response to certain actions, such as clicking a button, submitting a form, or scrolling through content.

# Event handling

We can create event handler to execute when an event occurs. An event handler is also known as an event listener, it could be a function with explicit name or an anonymous function.

# Inline event handler

We can assign an event handler or function to an event associated with an HTML element by using the HTML attribute with the name of the event handler.

<button onclick="alert('hi')"> Click here </button>

In this case, when the button is clicked, an alert shows up.

However, we should avoid directly assigning event handlers using the HTML event handler attributes, because:

  1. Mixing HTML and JavaScript makes it harder to maintain and debug.
  2. If element loads fully before JS code, then user could start interact with the page before event handler is in place.
  3. We need to be escaping HTML characters such as & or <.

# DOM event handler

Now to improve upon the previous inline event handler, we could separate the HTML and JavaScript.

const button = document.getElementById("button");
button.onclick = function () {
    alert("hi");
};

This allows us to dynamically assign and reassign event handlers. This is a simple way to create event handlers and is still widely used because of its cross-browser support.

# Event listener

All DOM nodes also have two helpful methods to register and unregister event listeners.

  • addEventListener(eventName, eventHandlerFn, useCapture/options)
  • removeEventListener(eventName, eventHandlerFn, useCapture/options)
const button = document.getElementById("button");
// having multiple event handlers on the same event and object is supported
button.addEventListener("click", handleClick1);
button.addEventListener("click", handleClick2);

Note: The addEventListener() method is the recommended way to register an event listener. The benefits are as follows:

  1. It allows adding more than one handler for an event. This is particularly useful for libraries, JavaScript modules, or any other kind of code that needs to work well with other libraries or extensions.
  2. In contrast to using an onXYZ property, it gives you finer-grained control of the phase when the listener is activated (capturing vs. bubbling).
  3. It works on any event target, not just HTML or SVG elements.

When attaching a handler function to an element using addEventListener(), the value of this inside the handler will be a reference to the element. It will be the same as the value of the currentTarget property of the event argument that is passed to the handler. Note that arrow functions do not have their own this context.

my_element.addEventListener("click", function (e) {
    console.log(this.className); // logs the className of my_element
    console.log(e.currentTarget === this); // logs `true`
});

my_element.addEventListener("click", (e) => {
    console.log(this.className); // WARNING: `this` is not `my_element`
    console.log(e.currentTarget === this); // logs `false`
});

Available options: options is an object that specifies characteristics about the event listener.

  1. capture: boolean indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. This is default to false.
  2. once: boolean indicating that the listener should be invoked at most once after being added. If true, the listener will automatically be removed when invoked. Default to false.
  3. passive: boolean indicating that function specified by listener will never call preventDefault(). If a passive listener does call preventDefault(), the user agent (browser) will do nothing other than generate a console warning. Default to false. In Safari, it is default to true for wheel, mousewheel, touchmove, and touchstart events.
  4. signal: an AbortSignal. The listener will be removed when the given AbortSignal object's abort() method is called.

More on passive listeners

If an event has a default action, i.e. a wheel event that scrolls the container by default, the browser cannot start the default action until the event listener has finished because it does not know in advance whether the event listener might cancel the default action by calling preventDefault(). If the event listener takes too long to execute, this could cause a jank.

Jank: sluggishness in a user interface, usually caused by executing long tasks on the main thread, blocking rendering, or expending too much processor power on background processes.

By setting the passive option to true, an event listener declares that it will not cancel the default action, so the browser can start the default action immediately, without waiting for the listener to finish.

# Dispatching events

The dispatchEvent() method of the EventTarget sends an Event to the object synchronously invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent().

# Event flow

When we click a button, we are not only clicking the button, but also the button's container, the container's container, and eventually the whole web page.

Event flow explains the order in which events are received on the page from the element where the event occurs and propagated through the DOM tree.

# Event capturing

In the event capturing model, an event starts from the root of the DOM tree and trickles down to the target element.

When we click a button, the click event occurs in the following order:

  1. document
  2. html
  3. body
  4. button container
  5. button

We use capturing when we want to intercept events before they reach their intended targets. This is rarely used in real-world applications.

Event Capturing

# Event bubbling

After reaching the target element, the event bubbles up back to the root of the DOM tree:

  1. button
  2. button container
  3. body
  4. html
  5. document

Bubbling is more commonly used. It allows us to catch events as they bubble up, usually to a common parent for similar elements.

Event Bubbling

DOM level 2 events specify that event flow has three phases:

  1. Capture phase: The event goes down the DOM tree to the target element.
  2. Target phase: The event reaches the target element where it originated.
  3. Bubbling phase: The event bubbles up from the target element back to the root.

Event Flow

We can stop event propagation at any phase using the stopPropagation() method:

document.getElementById("child").addEventListener(
    "click",
    function (event) {
        event.stopPropagation();
    },
    false
);

# Keyboard events

# What happens with key press

When we press a character key once on the board, three keyboard events are fired in this order:

  1. keydown: fires when you press a key on the keyboard, fires repeatedly while you're holding down the key.
  2. keyup: fires when you release a key on the keyboard.
  3. keypress: fires when you press a character key (a, b, c, ...), also fires repeatedly when holding.

# Keyboard event properties

The keyboard events have two important properties: key and code. The key property returns the character that has been pressed. The code property returns the physical key code. (i.e. when clicking on "A" key, key is a DOMString of "A", and code is "KeyA").

There are also modifier states on the event property:

  1. shiftKey: A Boolean indicating if the shift key was pressed (true) or not (false) when the event fired.

  2. ctrlKey: A Boolean indicating if the control key was pressed (true) or not (false).

  3. altKey: A Boolean indicating if the alt key was pressed (true) or not (false).

  4. metaKey: A Boolean indicating if the meta key (Cmd on Mac, Windows key on Windows) was pressed (true) or not (false).

# Mouse events

# What happens with a click

When we click an element, there are three mouse events fire in the following order:

  1. The mousedown fires when we depress the mouse button on the element.
  2. The mouseup fires when you release the mouse button on the element.
  3. The click fires when one mousedown and one mouseup detected on the element. Note both must exist in that order.

If we depress the mouse button on an element and move the mouse off the element, and then release the mouse button. Only one mousedown event fires at that element.

# What happens when you move

The mousemove event fires repeatedly when we move the cursor around an element, even if it is only one pixel. This could cause the page to be slow, thus we should only register the mousemove event handler when we actually need it and immediately remove the handler when it's no longer in use.

The mouseover fires when the mouse cursor is outside the element and then move into the boundaries of the element. The mouseout event is the opposite.

The mouseenter and mouseleave pair has the same behavior except that they do no bubble the event and thus does not fire on the parent element when we move cursor over the its descendant elements.

# Getting screen coordinates

This is useful for interviews that ask you to draw some shapes on a canvas. The screenX and screenY properties of the event object passed to the mouse event handler returns the screen coordinates of the location of the mouse. This is in relation to the entire computer screen.

Screen Coordinates

The clientX and clientY on the other hand, provide the coordinates within the application's client area at which the mouse event occurred. This is in relation to the open web page within browser.

Client Coordinates

# Scroll events

We can scroll a document or element by:

  • Using scrollbar
  • Using mouse wheel
  • Using keyboard
  • Clicking on link
  • Calling function in JavaScript

We can register a scroll event handler like this:

targetElement.addEventListener("scroll", (event) => {
    // handle the scroll event
});

targetElement.onscroll = (event) => {
    // handle the scroll event
};

# Scrolling window

The window object has two properties related to the scroll events: scrollX and scrollY. These properties return the number of pixels that the document is currently scrolled horizontally and vertically. They are double-precision floating-point values so we could use Math.round() if we need integer.

The scrollX and scrollY start at 0. pageXOffset and pageYOffset are aliases of these two respectively.

# Scrolling element

Like the window object, we can attach a scroll event handler to any HTML element. However, to track the scroll offsets, we need to use scrollTop and scrollLeft instead of scrollX and scrollY.

# Scrolling optimization

We should apply event throttling to throttle how often the scroll event handler is called. This, together with passive events, can help prevent scroll janks.

function throttledScrollHandler(e) {
    return throttle((e) => scrollHandler(e), 300);
}

element.addEventListener("scroll", throttledScrollHandler, { passive: true });

# Scrolling into view

The Element interface's scrollIntoView() method scrolls the element's ancestor containers such that the element on which scrollIntoView() is called is visible to the user.

scrollIntoView();
scrollIntoView(alignToTop);
scrollIntoView(scrollIntoViewOptions);

Learn more (opens new window) about the parameter options here that allow us to define how fast we scroll and how we align the element in the view after scrolling.

# Focus events

focus event fires when element has received focus. blur event fires when element has lost focus. An element will lose focus if another element is selected. An element will also lose focus if a style that does not allow focus is applied, such as hidden.

focusin and focusout event pairs are the same as focus and blur except that they bubble their events.

# Event delegation

In JavaScript, if we have a large number of event handlers on a page, these event handlers will directly impact the performance due to:

  1. Each event handler is a function (object) that takes up memory. The more objects in the memory, the slower the performance.
  2. It takes time to assign and attach each event handler, which causes a delay in the interactivity of the page.

Event delegation is a technique for handling events more efficiently by taking advantage of the event propagation model (more specifically, event bubbling). Instead of adding event listeners to individual elements, you attach a single event listener to a parent element. This listener analyzes the bubbled events to find a match on child element.

Example:

let menu = document.querySelector("#menu");

// parent element handles bubbled events from children
menu.addEventListener("click", (event) => {
    let target = event.target;

    // identify child by id
    switch (target.id) {
        case "home":
            console.log("Home menu item was clicked");
            break;
        case "dashboard":
            console.log("Dashboard menu item was clicked");
            break;
        case "report":
            console.log("Report menu item was clicked");
            break;
    }
});