Events in Javascript

Events in Javascript

Events in JavaScript: A Comprehensive Guide

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?

💡
Events are a signal that something has happened in the browser, and we have two main types of events: system events such as DOMContentLoaded and user events such as click.

  • 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

💡
Because there's only one event handler at a time, we don't want to write code that takes a lot of time to execute, which could lead to an unresponsive web application!

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();
    }
}
💡
Once the update fn gets called then in our UI we can refresh the state and re-render the page ;)

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:

  1. Capture listeners are called on the way down from the root to the target

  2. Target listeners are attached to the target (Where the event starts from the tree)

  3. Bubble listeners are called on the way up from the target towards the root.

    The bubble phase will only occur if event.bubbles is true

    💡
    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 )

💡
Event.target is the current element that triggers the event; once you get the target, you can do all sorts of things with it. Modify its style (classList, style, etc ), add attributes to HTML, remove it from the DOM, etc

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

💡
You will have to go through the whole list and see what do you need :)

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.

💡
So every events that happens inside the DOM can inherit these properties from the event interface

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, dblclick, mouseup, mousedown.

💡
So we can access those property if it's a mouse event :)

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

💡
Bubbling is convenient. Don’t stop it without a real need; it is obvious and architecturally well thought out.

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

💡
Stay semantic; don't abuse

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

💡
const event = new Event(type,options);

  • 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

💡
That means that we can not trigger a key event on the paragraph element if we don't add tabIndex to <p> element

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)

💡
Usually, when we use click events to manipulate the X and Y of an element, keep in mind that the style of the element has to be set to other values than static (relative, absolute)

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

💡
(pageYOffSet / body.scrollHeight - innerHeight) * 100

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

💡
focus, blur and scroll do not propagate because simply it doesn't make sense

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

💡
The event handler will still get called by the event function, but instead of immediately acting as the event handler, we set a timeout. We also clear the previous timeout (if any) so that when the events occur close together (closer than the timeout delay), the timeout from the previous event will be canceled. But if the function is already executed, there's no way to stop it.

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

💡
Browsers provide something called web workers. A worker is another JS process that runs alongside the main script on its own timeline (there will be another blog covering this worker topic)

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

  1. JavaScript Stack: Executes the current script or function.

  2. Micro-task Queue: Once the stack is empty, the event loop will first handle all tasks in the Micro-task queue.

  3. Macro-task Queue: After all Micro-tasks have been processed, the event loop will handle the next task in the Macro-task queue.

💡
IMPORTANT!! Every callback function( will be called in the future) will be queued and depends on the context, it will be either in micro-task queue or 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)💡

💡
The browser usually tries to render the page 60 times per second, meaning a frame per 16 ms. This also implies that in order to render smoothly, we have to execute a single task (a macro task) and all of the microtask generated by that task within 16ms ( crazy)

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

💡
After each macrotask queue, the DOM can be re-rendered

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>

💡
Remember that microtasks are smaller tasks that should be executed as soon as possible. Microtasks have priority

💡
The browser rule is that after one microtask, it will re-render the page (mainline JS scripts, user's events, network's events, timer's events) and if inside the macro tasks there are callbacks or promises, then the browser will have to finish these to re-render the page

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)

💡
The takeaway is that the event loop can process one task at a time, and we can never be certain that timer handlers will execute exactly when we expect them to. When we setInterval to be called after 10 seconds, in reality, it can be executed at much longer times, for example, 34, 42, 50, 60, and 70 ms, depending on how busy the queue is.

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

💡
A script has become "unresponsive" if it has run nonstop for at least 5 seconds, while some other browsers will even silently kill any script running for more than 5 seconds

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

💡
We want to re-render the page bit by bit instead of updating the whole page, which could result in an unresponsive UI. Imagine that when we update the table and the users want to interact with the page, they will fire another click event so we can handle the click event between re-rendering of the page

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.