Web browsers (and server side engines like NodeJS and Deno) include an Event API which has become a
foundational concept in JavaScript, and is particularly important when we talk about Web Components. The two classes
that drive all these events are the Event
class, and the EventTarget
class. MDN has some great in depth guides for
dealing with Events that go beyond this guide:
What are events?
Events are a way to allow two different pieces of code to communicate with each other, without having to import or otherwise know about their APIs - instead they just need to know about the Event APIs. Using events, your code can notify other parts of your application when a change has happened.
A really nice feature of events is that they're loosely coupled. Code that dispatches events doesn't need to worry if no listeners exist, it can just dispatch an event, and if no listeners have been added then nothing will happen. An event can have lots of listeners too, and when an event gets dispatched it will call each listener in the order that they were added.
A single class can have as many different events as it wants, and call them in any order or at any time. All events
are keyed by type
, so for example a Timer class could have a 'start'
event for whenever the timer starts, a 'stop'
event for whenever the time stops, and a 'tick'
event for every second that passes while the timer is running.
Advanced logic like this allows for some really powerful systems.
Whenever these events are dispatched, a new piece of code can listen for the events by using the same Event type
,
for example a piece of code can listen to all 'start'
events to be notified when a the Timer starts. Rather than
continuously ask "has the timer started?", the code setting up the listener says "notify me when there's a start
event". The code responsible for starting the timer can then dispatch the start Event that will notify all listeners.
What does the EventTarget
target class do?
EventTarget
is a class that exists in the browser, and has the API to dispatch Events, and to attach listeners for
Events. Here's what the definition for the EventTarget
class looks like:
class EventTarget { addEventListener(type, listener, options) removeEventListener(type, listener, options) dispatchEvent(event) }
Sub classing Event Target
Everything that extends from EventTarget
gets these methods, and can use them. HTMLElement
extends from
EventTarget
which means all Built-in elements and any Web Components you create will also extend from it too. Lots
of built-in objects extend it too - it's not just for elements. Global like window
and document
also extend from
EventTarget
. A single browser session can send hundreds of events during a typical web page session. For example
almost all elements will dispatch events for mouse movement and clicks, key presses, touch events (if your device has a
touch screen) and more. These make for a safe, scalable, and uniform way for developers to write interactive apps.
To make a subclass of EventTarget
you'll need to make your class extends EventTarget
(or it can extends
another
class that itself extends EventTarget
). Here's an example of a timer class that extends EventTarget
:
class Timer extends EventTarget {} const mytimer = new Timer() // Now these are all available: mytimer.addEventListener() mytimer.dispatchEvent() mytimer.removeEventListener()
Listening to Events
An EventTarget
includes the addEventListener()
method, which can be called to listen to events. It expects two
arguments, with an optional third argument. The first argument is type
, which is a string of the event type, for
example 'click'
. The second is the callback listener
function, this is a function that gets called whenever the
event is dispatched. The listener function might never be called, it might be called once, twice, or even hundreds
of times, it all depends on how often the event is dispatched. Lastly you can pass in options
, which allows advanced
customization of event listeners. For now let's focus on the first two arguments. Here's an example of adding an event
listener:
const mytimer = new Timer() // This will call `console.log` whenever the // `start` event type is dispatched mytimer.addEventListener("start", (event) => { console.log("something started!") })
Dispatching Events
An EventTarget
also has a dispatchEvent()
method, which can be called to run attached event listeners. It takes one
argument, which is an Event
instance. Every time dispatchEvent()
is called, the EventTarget
will go through the
list of listeners that have been added (via addEventListener()
) and if the name
matches the Events name, then the
listener
will be called. Going back to the Timer
example, let's make some methods that dispatch events and add
some code to listen to them:
class Timer extends EventTarget { start() { this.dispatchEvent(new Event("start")) } pause() { this.dispatchEvent(new Event("paused")) } unpause() { this.dispatchEvent(new Event("unpaused")) } stop() { this.dispatchEvent(new Event("stop")) } } const mytimer = new Timer() mytimer.addEventListener("start", () => console.log("timer started!")) mytimer.addEventListener("stop", () => console.log("timer stopped!")) mytimer.start() // logs: "timer started!" mytimer.pause() // nothing is logged because there are no 'paused' listeners mytimer.stop() // logs "timer stopped!"
What does the Event
class do?
The Event
class represents an Event that all listeners will be passed as the Event propagates through the system. It's
an object that has lots of information about the event, and it can be extended to add more info. The Event
class
requires a type
argument that will set the .type
of the event object. This is the most important piece of
information as it determines which listeners get called. The Event
class also takes an optional second argument which
is the event options. Let's just focus on the .type
for now. You can read more about the second options argument in
the next section Events in detail.
It's common to subclass Event
for different types that have more specific properties. For example the built-in
KeyboardEvent
is used for events like 'keypress'
, 'keydown'
, and 'keyup'
and has additional properties such as
.key
which describes the keyboard key related to the event. Another example, the built-in MouseEvent
, has a
.button
property instead, which describes which mouse button was pressed.
You can extend from Event
for your own classes, and add new properties or change the constructor to add or remove the
required arguments. Going back to the Timer
class, let's add a TickEvent
subclass which gets dispatched with every
second that passes while the Timer is running. Rather than using the standard Event
class, our TickEvent
can include
a .count
property which represents the amount of ticks that have happened since the timer was started:
class TickEvent extends Event { // Our TickEvent class only takes a count, // we hard code the .type constructor(count = 0) { // hardcode the .type of the element to 'tick'. super("tick") this.count = count } } class Timer extends EventTarget { start() { this.dispatchEvent("start") // Call #tick(1) after one second setTimeout(() => this.#tick(1), 1000) } #tick(times) { this.dispatchEvent(new TickEvent(times)) // Call #tick(n + 1) after one second setTimeout(() => this.#tick(times + 1), 1000) } } const mytimer = new Timer() mytimer.addEventListener("tick", (event) => { console.log(`timer has ticked ${event.count} times!`) }) mytimer.start()