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:
- Mixing HTML and JavaScript makes it harder to maintain and debug.
- If element loads fully before JS code, then user could start interact with the page before event handler is in place.
- 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:
- 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.
- In contrast to using an onXYZ property, it gives you finer-grained control of the phase when the listener is activated (capturing vs. bubbling).
- 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.
capture
: boolean indicating that events of this type will be dispatched to the registered listener before being dispatched to anyEventTarget
beneath it in the DOM tree. This is default tofalse
.once
: boolean indicating that the listener should be invoked at most once after being added. Iftrue
, the listener will automatically be removed when invoked. Default tofalse
.passive
: boolean indicating that function specified by listener will never callpreventDefault()
. If a passive listener does callpreventDefault()
, the user agent (browser) will do nothing other than generate a console warning. Default tofalse
. In Safari, it is default totrue
forwheel
,mousewheel
,touchmove
, andtouchstart
events.signal
: anAbortSignal
. The listener will be removed when the givenAbortSignal
object'sabort()
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:
- document
- html
- body
- button container
- 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 bubbling
After reaching the target element, the event bubbles up back to the root of the DOM tree:
- button
- button container
- body
- html
- document
Bubbling is more commonly used. It allows us to catch events as they bubble up, usually to a common parent for similar elements.
DOM level 2 events specify that event flow has three phases:
- Capture phase: The event goes down the DOM tree to the target element.
- Target phase: The event reaches the target element where it originated.
- Bubbling phase: The event bubbles up from the target element back to the root.
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:
keydown
: fires when you press a key on the keyboard, fires repeatedly while you're holding down the key.keyup
: fires when you release a key on the keyboard.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:
shiftKey
: A Boolean indicating if the shift key was pressed (true) or not (false) when the event fired.ctrlKey
: A Boolean indicating if the control key was pressed (true) or not (false).altKey
: A Boolean indicating if the alt key was pressed (true) or not (false).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:
- The
mousedown
fires when we depress the mouse button on the element. - The
mouseup
fires when you release the mouse button on the element. - The
click
fires when onemousedown
and onemouseup
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
andscreenY
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.
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.
# 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:
- Each event handler is a function (object) that takes up memory. The more objects in the memory, the slower the performance.
- 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;
}
});