Javascript Event Handling - Deep Dive
An unopinionated research (white) paper on front end event handling
Table of Contents
- Introduction
- Overview
- Deep Dive
- Resources
Introduction
Objective
The article takes an impartial approach to researching event handling in various UI tools. The content is based on official documentation -- NOT on opinion.
Purpose
The purpose is to understand how the same "problem" was solved across these tools.
What this article is NOT
This article does not assess the pros and cons -- neither does it recommend one tool over another.
Overview
The world of Javascript evolves at a breakneck speed. For the longest time, a webpage would consist of a single, monolithic script file that handled everything - starting from enabling interactivity on the page to calling services and rendering content. The pattern has significant drawbacks. Monolithic architectures are difficult to scale and maintain in the long term. Especially at the enterprise level where there are several engineers contributing code, a monolithic architecture tends to become a spaghetti mess that is hard to debug.
The inherent nature of Javascript allows engineers to innovate over this pattern and come up with ingenious ways to tackle the drawbacks. There are many, many, front end libraries and frameworks out there these days, each with its own superpowers and opinionated ways of approaching the problem. As a result, modern-day developers are spoilt for choices when picking a system to build their applications.
Although the list of tools at the disposal of developers is exhaustive, not many have stood the test of time and battle. In this article, we will investigate the ones that have come out (fairly) unscathed, in an attempt to understand how they handle events.
Deep Dive
This section will deep dive into several popular, publicly available UI libraries and frameworks to investigate how they handle events. Let’s start with arguably the most popular.
Handling events in React
Event handling in React centers around ReactBrowserEventEmitter. The very first comment in the source code does a decent job of explaining how it works.
Summary of ReactBrowserEventEmitter
event handling:
Let's dive deep and break each of them down:
--> Top-level delegation is used to trap most native browser events. This may only occur in the main thread and is the responsibility of
ReactDOMEventListener
, which is injected and can therefore support pluggable event sources. This is the only work that occurs in the main thread.
React uses event delegation to handle most of the interactive events in an application. This means when a button
with an onClick
handler is rendered
<button onClick={() => console.log('button was clicked')}>Click here</button>
React does not attach an event listener to the button
node. Instead, it gets a reference to the document root where the application is rendered and mounts an event listener there. React uses a single event listener per event type to invoke all submitted handlers within the virtual DOM. Whenever a DOM event is fired, those top-level listeners initiate the actual event dispatching through the React source code — it re-dispatched the event for every handler. This can be seen in the source code of EventPluginHub.
--> We normalize and de-duplicate events to account for browser quirks. This may be done in the worker thread.
React normalizes event-types so that every browser, regardless of its underlying engines or whether it's old or modern, will have consistent event arguments. This means, across all browsers, devices, and operating systems, a click
event will have arguments like this
- boolean altKey
- boolean metaKey
- boolean ctrlKey
- boolean shiftKey
- boolean getModifierState(key)
- number button
- number buttons
- number clientX
- number clientY
- number pageX
- number pageY
- number screen
- number screenX
- DOMEventTarget relatedTarget
📖 Further reading: events supported in React, read this.
--> Forward these native events (with the associated top-level type used to trap them) to
EventPluginHub
, which in turn will ask plugins if they want to extract any synthetic events.
React considers the nature of each event and categorizes them into buckets. It has dedicated plugins built to manage events in each bucket. Each of these plugins are then in turn responsible for extracting and handling the various event types in that bucket. For instance, the SimpleEventPlugin
will handle events implemented in common browsers such as mouse and keypress events (source) and ChangeEventPlugin
will handle onChange
events (source). The final piece that unifies all the plugins into a single place and re-directs events to each plugin is the EventPluginHub
.
This opens the door for us to understand how React views events. React introduces the concept of SyntheticEvents
, which React defines as "implementation of the DOM Level 3 Events API by normalizing browser quirks". Basically, it is a wrapper around the browser's native event object with the same interface — and that it works identically across all browsers.
For React v16 and earlier, synthetic events utilize a polling mechanism. This mechanism ensures that the same object instance is used in multiple handlers, but it is being reset with new properties before every invocation and is then disposed of.
--> The
EventPluginHub
will then process each event by annotating them with "dispatches", a sequence of listeners and IDs that care about that event.
In the React ecosystem, a single event listener is attached at the document root for any one event-type. Since each event type will most likely have multiple handlers, React will accumulate the events and their handlers (source). Then, it will do relevant dispatches, which consist of event handlers and their corresponding fiber nodes. The fiber nodes are nodes in the virtual DOM tree. Fiber nodes are calculated using React’s Reconciliation algorithm, which is it's “diffing” algorithm to drive updates on the page.
📖 Further reading: React Fiber Architecture
📖 Further reading: React Reconciliation concept
--> The
EventPluginHub
then dispatches the events.
The final piece of the puzzle — the plugin hub goes through the accumulated information and dispatches the events, thus invoking the submitted event handlers (source).
Simple Demo
Here is a simple click handler demo implementation in React --> Link.
Handling events in Vue
In Vue, you create a .vue
file that contains a script
tag to execute javascript and a template
tag that wraps all markup (both DOM and custom elements). This is a self-contained instance of a Vue component that could also contain a style
tag to house the CSS.
--> Simple DOM events handling
Vue allows developers to bind events to elements using v-on:<event-name>
or in short, @<event-name
directive, and to store the state of the application in a data
prop. All the event handlers are stored similarly in a methods
prop on the same object.
// App.vue
<template>
<div id="app">
<HelloWorld :msg="msg" />
<button @click="greet('World', $event)">
click here
</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: { HelloWorld },
data: function () {
return { msg: "Vue" };
},
methods: {
greet: function (message, $event) { this.msg = message; }
}
}
</script>
The application will load with a message “Hello Vue”. When the button is clicked, the handler will set the message to World and display a “Hello World” message --> REPL. It is possible to access the original DOM event by passing in an object from the handler reference and accessing the event handler.
--> Event Modifiers
Although it is possible to access the DOM event object in the handler by simply passing it in, Vue improves developer experience by allowing to extend event handling by attaching ‘modifiers’ to it. This way, Vue will handle the modifiers for you instead of the developer calling those modifiers explicitly in their handlers. Multiple modifiers can be attached by using a dot delimited pattern. The full list of supported modifiers is as follows:
.stop
.prevent
.capture
.self
.once
.passive
Thus, a simple example would look like this
/* this will trigger the handler method only once */
<button v-on:click.stop.once="clickHandler">Click me</button>
Link --> REPL.
--> Key Modifiers
Vue has a feature to attach keyboard events in an almost identical fashion as regular event handlers. It supports a list of aliases with commonly attached keyboard events such as the enter
and tab
keys. The full list of aliases is given below:
.enter
.tab
.delete
(captures both the "Delete" and "Backspace" keys).esc
.up
.down
.left
.right
.space
A simple example would look like the following
<!-- only call `vm.submit()` when the `key` is `Enter` -->
<input v-on:keyup.enter="submit">
LInk --> REPL.
--> Custom Events
Vue handles publishing and subscribing to custom events. The caveat here is that every component that should listen for events should maintain an explicit list of those custom events. A simple example would look like this
// emit event
this.$emit('myEvent')
// bind to the event
<my-component v-on:myevent="doSomething"></my-component>
Unlike components and props, event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, v-on
event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so v-on:myEvent
would become v-on:myevent
-- making myEvent
impossible to listen to. Vue JS as a framework recommends using kebab-casing for event names.
Link --> REPL.
Angular is one of the first generation, opinionated frameworks that focus on building Single Page Applications (SPAs). Although it has gone through significant re-invention in recent times, it still falls short in several ways when compared to the more modern tools available to developers these days (some of which are discussed in this article). It is still, however, valuable to take a peek at how the framework binds and handles events.
Handling events in Angular (4.x and above)
Angular has a very specific syntax to bind and handle events. This syntax consists of a target event name within parentheses to the left of an equal sign, and a quoted template statement to the right (source).
A simple example of DOM event binding and handling looks like this
<button (click)="onSave()">Save</button>
When events are being bound, Angular configures an event handler for the target event — it can be used with custom events as well. When either the component or the directive raises the event, the handler executes the template statement. Then, the template statement acts in response to the event.
In Angular, it is possible to pass an $event object to the function handling the event. The shape of the $event
object is determined by the target event
. If the event is a native DOM element event, then the $event
object is a DOM event object. Lets look at a simple example (source)
<input
[value]="currentItem.name"
(input)="currentItem.name=$event.target.val"
/>
There are a couple of things happening here:
- The code binds to the
input
event of the<input>
element, which allows the code to listen for changes. - When the user makes changes, the component raises the
input
event. - The binding executes the statement within a context that includes the DOM event object,
$event
. - Angular retrieves the changed text by following the path
$event.target.value and updates the
name` property.
If the event belongs to a directive or component, $event
has the shape that the directive or component produces.
Link --> REPL.
Handling events in Svelte
In Svelte, you create a .svelte
file that is meant to self contain a component instance with its CSS, JS, and HTML, along with any custom elements that are needed.
--> Simple DOM events handling
A simple demo for a click handler will look like the following:
<script>
let name = 'world';
function update() { name = 'Svelte'; }
</script>
<span on:click={update}>Hello { name }</span>
This will print Hello World
on load but will update and print Hello Svelte
when the user clicks on h1
-> REPL. This is the general pattern in which DOM events such as click
, mousemove
, etc are implemented in Svelte (it supports inline handlers as well).
--> Event Modifiers
The system allows developers to add pipe-delimited modifiers to the event , such as preventDefault
and stopPropagation
. The handler function can accept an event
argument that also has access to these modifiers, but Svelte offers an improvement in developer experience by offering these shorthands. An example would look like the following:
<script>
function handleClick() { alert('This alert will trigger only once!'); }
</script>
<button on:click|once={ handleClick }>Click here</button>
Thus, the pattern looks like on:<event-name>|modifier1|modifier2|...
-> REPL. The full list of modifiers is below (source):
preventDefault
- callsevent.preventDefault()
before running the handler. Useful for client-side form handlingstopPropagation
- callsevent.stopPropagation()
, preventing the event from reaching the next elementpassive
- improves scrolling performance on touch/wheel events (Svelte will add it automatically where its safe to do so)nonpassive
- explicitly setpassive: false
capture
- fires the handler during the capture phase instead of the bubbling phase (MDN docs)once
- remove the handler after the first time it runsself
- only trigger handler ifevent.target
is the element itself
--> Dispatching events
In Svelte, a parent component can update state based on data dispatched from a child component using a function called createEventDispatcher
. The function allows the child component to emit a data object at a user-defined key. The parent component can then do as it pleases with it -> REPL (open console to see dispatched data object).
--> Event forwarding
The caveat to component events is that it does not bubble. Thus, if a parent component needs to listen to an event that is emitted by a deeply nested component, all the intermediate components will have to forward that event. Event forwarding is achieved by adding the custom data key on each wrapper component as we traverse up Svelte DOM. Finally, the parent component where the event needs to handle implements a handler for it -> REPL (open console to see demo).
--> Actions
The final piece in Svelte event handling is the implementation of actions
. Actions are element-level functions that are useful for adding custom event handlers. Similar to transition functions, an action function receives a node
and some optional parameters and returns an action object. That object can have a destroy
function, which is called when the element is unmounted -> REPL (borrowed from Svelte official resources).
📖 Further reading: Svelte official tutorials
📖 Further reading: Compile Svelte in your head
Handling events in jQuery
The primary benefit of using jQuery is that it makes DOM traversal and manipulation quite convenient. Since most browser events initiated by users are meant to provide UI feedback, this feature is handy. Under the hood, jQuery uses a powerful "selector" engine called Sizzle. Sizzle is a pure JS-CSS selector engine designed to be dropped into any host library.
Let’s look at the programming model and categories of how jQuery binds and handles events. The “source” links provided is the official documentation of the APIs and has additional information on how they work:
--> Browser Events
Source: Browser Events
jQuery can handle the following browser events out of the box.
.error()
: Bind an event handler to the "error" JS event (source).resize()
: Bind an event handler to the "resize" JS event, or trigger the on an element (source).scroll()
: Bind an event handler to the "scroll" JS event, or trigger the event on an element (source)
--> Document Loading
Source: Document Loading
jQuery provides a shortlist of out of the box APIs to handle events related to initial page load
jQuery.holdReady()
: Holds or releases the execution of jQuery's ready event (source)jQuery.ready()
: A Promise-like object that resolves when the document is ready (source).load()
: Bind an event handler to the "load" JS event (source).ready()
: Specify a function to execute when the DOM is fully loaded (source).unload()
: Bind an event handler to the "unload" JS event (source)
--> Form Events
Source: Form Events
jQuery provides a decent list of out of the box APIs to handle commonly occurring form events
.blur()
: Bind an event handler to the “blur” JS event, or trigger that event on an element (source).change()
: Bind an event handler to the “change” JS event, or trigger that event on an element (source).focus()
: Bind an event handler to the “focus” JS event, or trigger that event on an element (source).focusin()
: Bind an event handler to the “focusin” JS event (source).focusout()
: Bind an event handler to the “focusout” JS event (source).select()
: Bind an event handler to the “select” JS event, or trigger that event on an element (source).submit()
: Bind an event handler to the “submit” JS event, or trigger that event on an element (source)
--> Keyboard Events
Source: Keyboard Events
The following are out of the box APIs provided by jQuery to handle keyboard events
.keydown()
: Bind an event handler to the "keydown" JS event, or trigger that event on an an element (source).keypress()
: Bind an event handler to the "keypress" JS event, or trigger that event on an an element (source).keyup()
: Bind an event handler to the "keyup" JS event, or trigger that event on an an element (source)
--> Mouse Events
Source: Mouse Events
This is where jQuery begins to shine as far as event handling is concerned. It offers a large suite of mouse event binders out of the box for developers to use.
.click()
: Bind an event handler to the "click" JS event, or trigger that event on an an element (source).dblclick()
: Bind an event handler to the "dblclick" JS event, or trigger that event on an an element (source).contextmenu()
: Bind an event handler to the "contextmenu" JS event, or trigger that event on an an element (source).mousemove()
: Bind an event handler to the "mousemove" JS event, or trigger that event on an an element (source).mouseout()
: Bind an event handler to the "mouseout" JS event, or trigger that event on an an element (source).mouseover()
: Bind an event handler to the "mouseover" JS event, or trigger that event on an an element (source).mouseup()
: Bind an event handler to the "mouseup" JS event, or trigger that event on an an element (source).toggle()
: Bind an event handler to the "toggle" JS event, or trigger that event on an an element (source).hover()
: Bind an event handler to the "hover" JS event, or trigger that event on an an element (source).mousedown()
: Bind an event handler to the "mousedown" JS event, or trigger that event on an an element (source).mouseenter()
: Bind an event handler to the "mouseenter" JS event, or trigger that event on an an element (source).mouseleave()
: Bind an event handler to the "mouseleave" JS event, or trigger that event on an an element (source)
--> The Event Object
Souce: Event Object, Inside Event Handling Function
Event handlers in jQuery accept the event object as the first argument. This object has access to various properties and modifiers. Here is a list of the more commonly occurring ones:
event.currentTarget()
: The current DOM element within the event handling bubbling phase (source)event.target()
: The DOM element that initiated the event (source)event.data()
: Optional data object passed to the handler when the current executing handler is bound (source)event.preventDefault()
: If this method is called, the default action of the event will not be triggered (source)event.stopPropagation()
: Prevents the event from bubbling up the DOM tree, preventing any parent handlers from being notified of the event (source)
📕 Note: Information below this point is related to jQuery versions later than 1.6.4
--> The
.on()
Event Handler Attachment API
Source: The .on()
Event Handler Attachment API
Modern versions of jQuery provide an all-encompassing API to handle events -- the .on()
. This API is designed to bind almost all of the events listed above with one single stroke. It is the recommended way to bind events (according to official documentation) from jQuery - 1.7 version and onwards. A few syntax examples can be seen below:
// Markup to be used for all examples that follow
<div class='outer'>
<span class='inner'>Any content</span>
</div>
// Exhibit A: the simple click handler, targeting the inner span
$('.outer .inner').on('click', function(event) {
console.log(event);
alert( 'inner span was clicked!!' );
});
// Exhibit B: attaching separate handlers to different event types
$('.outer .inner').on({
mouseenter: function() {
console.log( 'hovered over a span' );
},
mouseleave: function() {
console.log( 'mouse left a span' );
},
click: function() {
console.log( 'clicked a span' );
}
});
// Exhibit C: attaching the same handler to different event types
$('.outer .inner').on('click', function() {
console.log( 'The span was either clicked or hovered on' );
});
// Exhibit D: Event delegation --> binding events to elements that don't exist yet
$('.outer .inner').on('click', '<selector-of-element-that-dont-exist-yet>', function() {
console.log( 'The element was clicked' );
});
--> Other Event Handler Attachment APIs
Source: Event Handler Attachment
The .on()
API is arguably the most popular API offered by jQuery. Apart from it, there are other interfaces jQuery has out of the box that provides a useful suite of functionality. The following is a list of the most commonly occurring ones:
one()
: Attach a handler to an event for the elements. The handler is executed at most once per element per event type (source)off()
: Remove an event handler (source)trigger()
: Execute all handlers and behaviors attached to the matched elements for the given event type (source)
Resources
- List of front end JS frameworks
- React
- Svelte
- Vue
- Angular
- jQuery