Table of contents
- High-level DOM Event concepts
- Event Handling at High-level
- Observer patterns with events
- Event Target
- How many ways can we attach events to an HTML element?
- Event Listener Mechanism
- Event
- More specific interface
- Event with Typescript
- Stop bubbling (stop propagation)
- Event delegation
- Browser default actions
- Custom Event
- Key Event
- Focus Events
- Events and Event loop
- Timers
- Summary
Events in JavaScript empower developers to respond to user interactions, browser actions, and other asynchronous occurrences with precision and finesse. In this comprehensive exploration, we'll delve into the fundamental concepts of events, their types, and how they work. We'll also discuss how to attach event listeners, handle events, and manage event propagation. From basic click events to more complex custom events, we'll equip you with the knowledge and skills needed to bring your web applications to life.
High-level DOM Event concepts
Event
To understand this picture, we'll need to go through some basic terminology, but first of all, what is an event?
Browser events (finished loading a page, DOM finished loading)
Network events (fetch event, server-side events)
User events (click, mouse move, etc)
Timer events (setTimeOut,setInterval, etc)
Event Handling at High-level
The browser execution environment, at its core, only handles one function at a time
Example of event flow
Observer patterns with events
Real-life
Observer Pattern defined: the class diagram
interface Observer {
update:(tempature:number,humidity:number,pressure:number)=>void;
}
interface Subject {
registerObserver:(observer:Observer)=>void;
removeObserver:(observer:Observer)=>void;
notifyObserver:()=>void;
}
class WeatherData implements Subject {
private observers:Observer[];
private tempature:number = 0;
private humidity:number = 0;
private pressure:number = 0;
constructor(){
this.observers = [];
}
public registerObserver(o:any) {
this.observers.push(o);
}
public removeObserver(o:Observer) {
this.observers.filter((observer)=>observer !== o);
}
public notifyObserver() {
this.observers.forEach((observer)=>observer.update(this.tempature,this.humidity,this.pressure));
}
public measureChange() {
this.notifyObserver();
}
public setMeasurements(tempature:number,humidity:number,pressure:number) {
this.tempature=tempature;
this.humidity=humidity;
this.pressure=pressure;
this.measureChange();
}
}
Observer patterns are the key to understanding how events are subscribed in JS
class Observer {
constructor() {
this.observers = new Map();
}
addObserver(key,observerFn) {
if(!this.observers.has(key)) {
this.observers.set(key,new Set())
}
this.observers.get(key).add(observerFn)
}
removeObserver(key, observerFn) {
const observerCollections = this.observers.get(key);
if (observerCollections) {
observerCollections.delete(observerFns);
if (observerCollections.size === 0) {
this.observers.delete(key);
}
}
}
notifyObservers(key, data) {
const observerCollections = this.observers.get(key);
if (observerCollections) {
observerCollections.forEach(observer => {
observerCollections(data);
});
}
}
}
Event Target
An event target is any object that implements the EventTarget
interface (eg window
, Element
and XMLHttpRequest
)
An event target can be the target of events and can have event listeners added and removed from them
Phrase
When an event target participates in a tree 🌲 the event flows through the tree in three phases:
Capture listeners are called on the way down from the root to the target
Target listeners are attached to the target (Where the event starts from the tree)
Bubble listeners are called on the way up from the target towards the root.
The bubble phase will only occur if
event.bubbles
istrue
💡Most events are bubble, except focus events and other rare event listeners that we'll meet later and by default capture is turned off; we need to set it to true to activate it
Explain it in simple terms
event target vs eventCurrentTarget
A handler on a parent element can always get the details about where it happened
The most deeply nested element that caused the event is called a target element ( event target )
event. target is the target element that initializes the events and doesn't change through the bubble process
event.currentTarget is the current element (which usually changes through the bubble process )
How many ways can we attach events to an HTML element?
HTML
<!-- In HTML or (React way) -->
<button
onclick="console.log(this.event)"
type="button">
Call to action
</button>
Query a document element and assign property or addEventListener
const button = document.querySelector("button");
if(button === null ) {
throw new Error("Unable to query the button");
}
//Object property event handler
button.onclick = function onClick(e)=> {
console.log(e);
}
//Attach an event listener to button
button.addEventListener("click",(e)=> {
console.log("onClick",e);
}
Event Listener Mechanism
/* Best is to use addEventListener */
/* It allows multiple event listeners to be added */
button.addEventListener("click",onClick);
button.addEventListner("click",onClick2);
/* A lot control over binding (capture) */
button.addEventListner("click",onClick,{capture:true });
/* Some events (system events) do not have corresponding HTML
attribute or object property event handler */
window.addEventListener("DOMContentLoaded" , function log () {
console.log('DOMContentLoaded');
});
Remove Event Listener
Event
Events name
Listen to these events using addEventListener()
or by assigning an event listener to the oneventname
property of this interface.
https://developer.mozilla.org/en-US/docs/Web/API/Element#events
https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L8267
Events interface
The Event
interface represents an event that takes place in the DOM.
More specific interface
To properly handle the event, we want to know more about what's happened. Not just a "click" or a "key-down", but what were the pointer coordinates? Which key was pressed? And so on
When an event happens, the browser creates an event object, puts details into it and passes it as an argument to the handler
<input type="button" value="Click me" id="elem">
<script>
elem.onclick = function(event:any ??? ) {
// Some of the generic event that we can access
// event.target || event.currentTarget || event.preventDefault();
alert(event.type + " at " + event.currentTarget);
alert("Coordinates: " + event.clientX + ":" + event.clientY);
};
</script>
Click is indeed a MouseEvent interface
The MouseEvent
interface represents events that occur due to the user interacting with a pointing device (such as a mouse). Common events using this interface include click
, dbl
click
, mouseu
p
, mouse
down
.
Event with Typescript
We also have an event hierarchy, just like we have with the DOM element. We'll have a base Event that shares all the common properties to other events
More specific types include:
UIEvent (any sort of user interface event)
MouseEvent (An event triggered by the mouse, such as click)
TouchEvent (A touch event on mobile)
WheelEvent (An event triggered by rotating the scroll wheel)
KeyboardEvent(A key press)
const mouseDown = (el:MouseEvent)=> {
el.clientX, el.clientY // correct
//currentTarget has an interface EventTarget,this is so generic
//and if we re gonna have classList property it should be a HTML element
const target = el.currentTarget as HTMLElement; //
target.classList.add("dragging");
}
Stop bubbling (stop propagation)
A bubbling event goes from the target element straight up. Normally it goes upwards until <html>
, reaches to document
object, and some events even reach it window
, calling all handlers on the path.
But any handler may decide that the event has been fully processed and stop the bubbling.
The method for it is event.stopPropagation()
.
Stop multiple event handles with e.stopImmediatePropagation
Event delegation
Capturing and bubbling allow us to implement one of the most powerful event-handling patterns, called event delegation.
Browser default actions
Some of the actions automatically trigger an event for us
A click on a link navigates to a different page ( it will cause the browser to refresh the page)
A click on the form submit button initiates its submission to the server
Pressing the mouse button over a text will select the text
JS gives us the option to stop these automatic events
const anchor = document.querySelector("a");
anchor.addEventListener("click",(e)=>{
e.preventDefault();
});
Create an image gallery where the main image changes by clicking on a thumbnail
Technically, by preventing default actions and adding JavaScript we can customize the behavior of any elements. For instance, we can make a link <a>
work like a button, and a button <button>
behaves as a link (redirect to another URL or so).
But we should generally keep the semantic meaning of HTML elements. For instance, <a>
should perform navigation, not a button.
Besides being “just a good thing”, that makes your HTML better in terms of accessibility.
Custom Event
In FE worlds, where HTML is not enough, we create our components (toggle button, menu, tree, etc.). Same for the event; we can create our own event
type: event type, a string like "click" or our own like "my-event"
options: the object with two optional properties
bubbles: true/false if true, then the event bubbles
cancelable: true/false if true, then the default action may be prevented
By default: bubbles and cancelable are false
Dispatch an event
After an event is created, we have to dispatch it through an element
const button = document.querySelector("button");
if ( button === null ) { return ; }
button.addEventListner("hello",()=>{console.log("Run my hello event");})
const event = new Event("hello");
button.dispatchEvent(event);
Key Event
https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L25369
interface GlobalEventHandlersEventMap {
"abort": UIEvent;
"animationcancel": AnimationEvent;
"animationend": AnimationEvent;
"animationiteration": AnimationEvent;
"animationstart": AnimationEvent;
"auxclick": MouseEvent;
"beforeinput": InputEvent;
"beforetoggle": Event;
"blur": FocusEvent;
"cancel": Event;
"canplay": Event;
"canplaythrough": Event;
"change": Event;
"click": MouseEvent;
"close": Event;
"compositionend": CompositionEvent;
"compositionstart": CompositionEvent;
"compositionupdate": CompositionEvent;
"contextlost": Event;
"contextmenu": MouseEvent;
"contextrestored": Event;
"copy": ClipboardEvent;
"cuechange": Event;
"cut": ClipboardEvent;
"dblclick": MouseEvent;
"drag": DragEvent;
"dragend": DragEvent;
"dragenter": DragEvent;
"dragleave": DragEvent;
"dragover": DragEvent;
"dragstart": DragEvent;
"drop": DragEvent;
"durationchange": Event;
"emptied": Event;
"ended": Event;
"error": ErrorEvent;
"focus": FocusEvent;
"focusin": FocusEvent;
"focusout": FocusEvent;
"formdata": FormDataEvent;
"gotpointercapture": PointerEvent;
"input": Event;
"invalid": Event;
"keydown": KeyboardEvent;
"keypress": KeyboardEvent;
"keyup": KeyboardEvent;
"load": Event;
"loadeddata": Event;
"loadedmetadata": Event;
"loadstart": Event;
"lostpointercapture": PointerEvent;
"mousedown": MouseEvent;
"mouseenter": MouseEvent;
"mouseleave": MouseEvent;
"mousemove": MouseEvent;
"mouseout": MouseEvent;
"mouseover": MouseEvent;
"mouseup": MouseEvent;
"paste": ClipboardEvent;
"pause": Event;
"play": Event;
"playing": Event;
"pointercancel": PointerEvent;
"pointerdown": PointerEvent;
"pointerenter": PointerEvent;
"pointerleave": PointerEvent;
"pointermove": PointerEvent;
"pointerout": PointerEvent;
"pointerover": PointerEvent;
"pointerup": PointerEvent;
"progress": ProgressEvent;
"ratechange": Event;
"reset": Event;
"resize": UIEvent;
"scroll": Event;
"scrollend": Event;
"securitypolicyviolation": SecurityPolicyViolationEvent;
"seeked": Event;
"seeking": Event;
"select": Event;
"selectionchange": Event;
"selectstart": Event;
"slotchange": Event;
"stalled": Event;
"submit": SubmitEvent;
"suspend": Event;
"timeupdate": Event;
"toggle": Event;
"touchcancel": TouchEvent;
"touchend": TouchEvent;
"touchmove": TouchEvent;
"touchstart": TouchEvent;
"transitioncancel": TransitionEvent;
"transitionend": TransitionEvent;
"transitionrun": TransitionEvent;
"transitionstart": TransitionEvent;
"volumechange": Event;
"waiting": Event;
"webkitanimationend": Event;
"webkitanimationiteration": Event;
"webkitanimationstart": Event;
"webkittransitionend": Event;
"wheel": WheelEvent;
}
interface GlobalEventHandlers {
addEventListener<K extends keyof GlobalEventHandlersEventMap>(type: K, listener: (this: GlobalEventHandlers, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
}
When a key is pressed, your browser fires a "key-down" event. When it is released, you get a "keyup" event
<script>
window.addEventListener("keydown", event:KeyEvent) =>
{
if (event.key == "v") {
document.body.style.background = "violet";
}
}
);
window.addEventListener("keyup", event:KeyEvent) => {
if (event.key == "v") {
document.body.style.background = "";
}
}
);
//Look at key combinations
window.addEventListener("keydown", event:KeyEvent) => {
if(event.key =="" & event.ctrlKey) {
console.log("Continuing");
}
})
</script>
The DOM node where a key event originates depends on the element that has focus when the key is pressed. Most nodes cannot have focus unless you give them a tabindex attribute, but things like links, buttons, and form fields can. When nothing in particular has focus, document.body acts as the target node of key events
https://stackoverflow.com/questions/31636337/javascript-keydown-event-wont-fire-on-paragraph
Pointer Event
Pressing a mouse button causes a number of events to fire. The "mousedown" and "mouseup" events are similar to "keydown" and "keyup" and fire when the button is pressed and released
mousedown/mouseup
mouseover/mouseout (hover)
mousemove
click
dblclick
context menu (trigger when the right button is clicked)
Events order
The user can trigger multiple events ( mouse down -> mouse up -> click)
MouseEvent
To get precise information about the place where a mouse event happened, you can look at some of these properties
offSetX/offSetY: event's coordinates ( in pixels ) relative to the parent container
clientX/clientY: coordinates relative to the top left corner for the window
pageX/pageY: coordinates relative to the top left corner of the entire page (when the window has been scrolled)
Mouse move
When you click on the element and drag it, a "mousedown" event is fired
Every time a mouse pointer moves, a "mousemove" event is fired. This event can be used to track the position of the mouse.
When you release the mouse, a "mouseup" event is fired. And possibly we want to cancel the "mousemove" event
Touch events
Pretty similar to the mouse move event. We have "touchstart", "touchmove" and "touchend"
Scroll events (window event)
Whenever an element is scrolled, a "scroll" event is fired on it
We use % rather than px as a unit when setting the width so the element is sized relative to the page width
Focus Events
When an element gains focus, the browser fires a "focus" event on it. When it loses focus, the element gets a "blur" event
Load event
When a page finishes loading, the "load event" fires on the window and the document body objects. This is mainly used when we want to fire some actions that require the whole document to have been built
Elements such as images or script tags that load an external file also have a "load" event that indicates the files they reference were loaded. Like focus-related events, loading events do not propagate
When a page is closed or navigated away from (for example, by following a link), a "before unload" event fires. The main use of this is to prevent users from accidentally losing work by closing a document.
Media event
Limit event streams with debounce
Some types of events have the potential to fire rapidly, many times in a row (the "mousemove" and "scroll" events, for example)
If you do need to do something nontrivial in such a handler, you can use setTimeout to make sure you're not doing that often. This is usually called debouncing the event. There are several slightly different approaches to this
React examples
Because of the nature of single-threaded language like JS, bringing the concept of that to ReactJS is the same thing
(() => {
const toggleVisibility = () => {
//detect if the page has been scroll to the bottom
};
const debounceToggle = debounce(toggleVisibility, 100);
window.addEventListener("scroll", debounceToggle);
return () => window.removeEventListener("scroll", debounceToggle);
}, [detectDevice]);
const [text,setText] = React.useState("");
//This also has a performance issue when users typing quick , the
//UI will be hard to follow :) the solution is useDefferedValue
return (
<main>
<input
type="text"
value={text}
onChange={({currentTarget})=>setText(currentTarget.value)} />
{
[...Array(30000)].map(()=>
<p> {text} </p>
}
</main>
)
Summary
Pressing a key fires "keydown" and "keyup" events with focus mode on element
Pressing a mouse button fires "mousedown", "mouseup" and "click" events. Moving the mouse fires "mousemove" events
Touchscreen interaction fires "touchstart","touchmove" and "touchend" events
Scrolling can be detected with "scroll" event
Focus can be detected with "focus" events and "blur" events
When the document finishes loading, a "load" event fires on the window
Events and Event loop
In the context of events, the events can only be processed when nothing is running, which means that if the event loop is tied up with other work, any interaction with the page (which usually happens through the events) will be delayed until there's time to process it. So if you schedule so much work, either with long-running event handlers or with a lot of short-running ones, the page will become slow and cumbersome to use
Usually, when we have a complicated UI that has many events happening at the same time, the JS engine needs to decide what tasks they should run first and so on
JavaScript Stack: Executes the current script or function.
Micro-task Queue: Once the stack is empty, the event loop will first handle all tasks in the Micro-task queue.
Macro-task Queue: After all Micro-tasks have been processed, the event loop will handle the next task in the Macro-task queue.
Macrotask queue ( such as creating main document object, parsing HTML, executing mainline JS code, changing the URL, as well as various events such as page loading, input, networking events and timer events )
Microtask queue ( smaller tasks that update the application state and should be executed before the browser re-renders the UI )
60 FPS (frame per second)💡
Be careful about which events you decide to handle, how often they occur, and how much processing time an event handler takes. Moving the mouse around causes large events to be queued, so performing any complex operation in that mouse-mouse handler is overkill for our app
Scenarios
Because the browsers are trying to render within 16 ms, the event loop often asks, "Is rendering required?" for every 16 ms. Executing the macrotask and all of its related microtasks takes much more than 16 ms. In this case, the browser won't be able to render the page at the target frame rate, and the UI won't update. If we have some animations running on the page, users will feel the page is slow and unresponsive.
An example of macrotasks
JS is a single-threaded execution model and can handle only one at a time
Let's have an example
We have global JS code to execute the mainline
We have two buttons and two clicks to the buttons, one for each button
An example of both microtask and macrotasks
Macrotasks are kinds of stuff like promise and DOM manipulations
<script>
const firstButton = document.getElementById("firstButton");
const secondButton = document.getElementById("secondButton");
firstButton.addEventListener("click",()=> {
Promise.resolve().then(()=>{
});
/* click will run after 5ms quick users has clicked on it */
})
secondButton.addEventListener("click",()=> {
Promise.resolve().then(()=>{
});
/* click will run after 8ms quick users has clicked on it */
})
/* Code that run for 15s */
</script>
Timers
Timers give us the ability to break long-running tasks into smaller tasks that won't block the event loop
Timers aren't defined in JS itself; instead, they're provided by the host environment (browser or nodeJS)
setTimeout (repeat calling a function after ms time)
setInterval(calling a function every ms time)
Unlike the setTimeout function, which expires only once, the setInterval function fires until we explicitly clear it. So, at around 20 ms, another interval fires. Normally, this would create a new task and add it to the task queue. But this time, because an instance of an interval task is already queued and awaiting execution, this invocation is dropped. The browser won’t queue up more than one instance of a specific interval handler (probably for optimization)
set timeout vs setInterval
setTimeout(function repeatMe() {
/** doing some works **/
setTimeout(repeatMe,10);
},10);
setInterval(()=>{
/** doing some works **/
},10);
Notably, the setTimeout variant of the code will always have at least a 10ms delay after the previous callback execution (depending on the state of the event queue, it may end up being more but never less)
Streaming (handle bit by bit)
JS is single-threaded and if we have many event listeners or some heavy computing on JS, the browser may stutter or seem to hang because all updates to the rendering of a page are suspended while JS is executing
In these situations, timers can come to the rescue and become especially useful because they are capable of effectively suspending the execution of a piece of JS until a later time
<body>
<table>
<tbody></tbody>
</table>
</body>
<script>
// This piece of will run long time in callstack
// tr.appendChild just a function that manipulate the DOM
// not in queue
const tbody = document.querySelector("tbody");
for (let i = 0; i < 20000; i++) {
const tr = document.createElement("tr");
for (let t = 0; t < 6; t++) {
const td = document.createElement("td");
td.appendChild(document.createTextNode(i + "," + t));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
</script>
- Because of the single-threaded execution model, tasks are processed one at a time, and after a task starts executing, it can’t be interrupted by another task.
In this example, we're creating a total of 240.000 DOM nodes, populating a table with 20.000 rows of 6 cells. This will likely hang the browser
It's very similar to this mindset when handling streaming data on NodeJS
Summary
Events in JS are such an important topic in JS land. And in their simple form, events are still quite hard to wrap my heads around, and it's not obvious how events work in JS. More than that, events can be triggered multiple times, and we have to handle our code well to provide smooth interactions on the web because we already know that JS is single-threaded and firing multiple events can be a dead end for our customers. And you already know why, hopefully after this blog. That's it, folks. Until next time, see you.