SHIFT + D

Listenable

Updated on August 22, 2024Source codeTests

Listenable is a class that enriches an event type (including observation types, media queries, custom gestures, and more), allowing it to:

  • Listen for that event type and execute a callback function when it occurs
  • Retrieve a list of active listeners that it has added
  • Store a status (ready, listening, or stopped)
  • Easily clean up all listening activity to avoid memory leaks

Example

Baleada's docs use Listenable to:

  • Toggle the dark theme when you press Shift + D
  • Toggle the minimalist theme when you press Shift + M
  • Set your default theme (dark or light) based on your device's color theme preference
  • On mobile, open and close the sidebar nav and table of contents when you swipe right or left

Construct a Listenable instance

The Listenable constructor accepts two parameters:

Parameter
Type
Required
Description
type
String
yes
The type of event that will be made listenable. See the Valid event types section for more guidance.
options
Object
no
Options for the Listenable instance. See the Listenable constructor options section for more guidance.

Valid event types

Listenable supports a ton of different event types and can deduce which web APIs to use under the hood based on the type you pass.

Most of the time, you don't need to be concerned with exactly which web API is being used, and you can think of it as an implementation detail.

But in certain cases where you want to customize the way a specific web API behaves, you'll need to know which API is being used in order to know what customization options are available.

You'll find more guidance down below, in the How to customize listen behavior section.

The table below has a breakdown of valid event types, the corresponding web APIs that Listenable uses under the hood, and the main purpose of the API.

Event type
Web APIs
Purpose
Every event listed in TypeScript's HTMLElementEventMap and DocumentEventMap, e.g. click, keydown, mousemove, etc.
Listens for basic events.
intersect
Listens for DOM elements intersecting with an ancestor element or with the top-level document's viewport.
resize
Listens for DOM elements being resized
mutate
Listens for DOM element being mutated (e.g. children added or removed)
Media queries (i.e. any valid first argument for the matchMedia method)
Listens for changes to browser metadata (e.g. screen size, or color scheme preference)
idle
Listens for the end user going idle
message and messageerror
Listens for messages and/or errors from a BroadcastChannel
Listens for the end user going idle
recognizeable
A nested Listenable instance 🤯

Listens for custom gestures, powered by Recognizeable.

See the How to listen for custom gestures section for more guidance.

Listenable constructor options

Option
Type
Default
Description
recognizeable
Object
none

Passes options for a new instance of the Recognizeable class. See the How to listen for custom gestures section for more guidance.

The recognizeable option only has an effect when your type is recognizeable.

State and methods

Property
Type
Description
Parameters
Return value
type
Getter/Setter
See return value
N/A

The event type you passed to the Listenable constructor.

If you assign a value directly to type, a setter will pass the new value to setType.

status
Getter
See return value
N/A

The status (String) of the Listenable instance.

status is ready after the instance is constructed, and changes to listening after the listen method is called for the first time, and change to stopped after all web API activity has been stopped and cleaned up.

active
Getter
See return value
N/A
A Set of objects that describe all the currently active listeners, observers, etc.
recognizeable
Getter
See return value
N/A

The Recognizeable instance constructed using the options you passed to options.recognizeable.

If you didn't pass that option, the recognizeable property will be undefined.

See the How to listen for custom gestures section for more guidance.

setType(type)
Function
Sets a new type, after stopping and cleaning up all existing web API activity.
The new type (String).
The Listenable instance
listen(effect, options)
Function
Listens for the events specified by your type, and performs side effects via a callback function when the events happen. Can't be called until the DOM is available.

A side effect (Function, required) that will be performed when the events are detected, and an optional options object.

To learn more about handling events with your side effect function, see the How to handle events and How to customize listen behavior sections.

The Listenable instance
stop(options)
Function
Stops and cleans up web API activity. Can't be called until the DOM is available.

An optional object with a target property, whose value is a DOM element, window, document, or a BroadcastChannel instance, depending on what type of event you're listening for.

If options.target is passed, only activity related to that target will be stopped.

If options.target is not passed, all activity is stopped.

See the Default values for

The Listenable instance

How to handle events

Depending on your type, your effect—passed as the required first argument of the listen method—will receive different parameters.

The table below has a full breakdown of what the listen method passes to your effect for each specific event type:

type
What your effect receives
Every event listed in TypeScript's HTMLElementEventMap and DocumentEventMap, e.g. click, keydown, mousemove, etc.
The corresponding DOM event
intersect
An array of IntersectionObserverEntry objects
resize
An array of ResizeObserverEntry objects
mutate
An array of MutationRecord objects
Media queries (i.e. any valid first argument for the matchMedia method)
idle
An IdleDeadline object
message and messageerror
recognizeable

The latest sequenceItem added to your Recognizeable instance's sequence.

Often, your effect won't actually do anything with this argument. Instead, it will reach into listenableInstance.recognizeable.metadata for additional information about the captured sequence of events.

How to customize listen behavior

The listen method accepts an optional second parameter, which is an Object whose properties customize the behavior of the web APIs Listenable uses under the hood.

Depending on your type only certain properties will have an effect.

First, here's a breakdown of what each options property does, and below that, in the Available options for each type section, you'll find a table of which properties can be used for each type:

Option
Type
Default
Description
target
HTMLElement, Document
See description

The target that will listen for events.

See the Default values for target based on type section for more guidance on default target values.

addEventListener
Object
none
The options parameter of addEventListener
useCapture
Boolean
none
A value for the standalone useCapture parameter of addEventListener. Ignored if an addEventListener object was passed.
wantsUntrusted
Boolean
none
A value for the standalone wantsUntrusted parameter of addEventListener
except
Array
[]

An array of DOM elements that, if they are the target of the event, should not cause your effect to be executed.

When the only option is a non-empty array, except is ignored.

only
Array
[]

An array of DOM elements that, if they are the target of the event, should cause your effect to be executed.

When only is a non-empty array, except is ignored.

An empty only array is ignored (otherwise, the effect would never execute).

observer
Object
none
The options parameter of the Intersection Observer constructor
observe
Object
none
The options parameter of the MutationObserver.observe and ResizeObserver.observe methods
requestIdleCallback
Object
none
The options parameter of requestIdleCallback
keyDirection
String
down

Indicates which keyboard event should be listened to when detecting keycombos. Valid options are down and up .

The keyDirection option only has an effect when your type is a keycombo, as described the How to format combos.

Default values for target based on type

An object with a `target` property, which should be the `BroadcastChannel` instance that is receiving messages.

The default value for the listen method's target option depends on your type. The table below has a full breakdown.

type
Default target
Every event listed in TypeScript's HTMLElementEventMap, e.g. click, keydown, mousemove, etc.
document
Every event listed in TypeScript's DocumentEventMap and not listed in the HTMLElementEventMap, e.g. visibilitychange, fullscreenchange, etc.
document (can't be overridden)
intersect
document.querySelector('html')
resize
document.querySelector('html')
mutate
document.querySelector('html')
Media queries (i.e. any valid first argument for the matchMedia method)
N/A
idle
N/A
message and messageerror

A new BroadcastChannel.

You'll technically be able to retrieve this BroadcastChannel from listenableInstance.active and send messages from it, but the DX of creating and passing in your own BroadcastChannel is much better.

Be sure to check out Baleada's Broadcastable class, and especially the Using Broadcastable with Listenable docs, if you're interested in handling BroadcastChannel messages with Baleada and Listenable.

recognizeable
The corresponding default for each type handled by your Recognizeable instance's effects

Available options for each type

type
Available options
Every event listed in TypeScript's HTMLElementEventMap, e.g. click, keydown, mousemove, etc.
  • target
  • addEventListener
  • useCapture
  • wantsUntrusted
  • except
  • only
Every event listed in TypeScript's DocumentEventMap and not listed in the HTMLElementEventMap, e.g. visibilitychange, fullscreenchange, etc.
  • addEventListener
  • useCapture
  • wantsUntrusted
intersect
observer
resize
observe
mutate
observe
Media queries (i.e. any valid first argument for the matchMedia method)
none
idle
requestIdleCallback
message and messageerror
target
recognizeable
None

How to listen for custom gestures

Listenable allows you to listen for custom gestures defined by Baleada Logic's Recognizeable class.

Before you read any further, it's worth checking out the usage docs for the gestures that Baleada supports out of the box.

Those docs will show you the overall workflow for using Listenable, with Recognizeable under the hood, to recognize complex gestures.

If you get a better understanding of that system and want to define a gesture of your own, definitely visit the Recognizeable docs for more guidance.

Using with TypeScript

Listenable will type check your listen method effect functions and options based on the type (String) you pass to the constructor:

const instance = new Listenable('intersect')

instance.listen(
  // Listenable infers from the `intersect` event type that
  // `entries` is an array of IntersectionObserverEntry objects.
  //
  // entries[0].boundingClientRect.width is correctly typed
  // as a number, automatically!
  entries => console.log(entries[0].boundingClientRect.width),
  // Listenable also knows that the `listen` method for an
  // 'intersect' type can accept an `observer` option,
  // passing an IntersectionObserverInit object.
  { observer: { threshold: 0.5 } }
) 

Even complex types like media queries will be detected:

const screenSize = new Listenable('(min-width: 420px)')
screenSize.listen(
  // TypeScript knows that this is a MediaQueryListEvent
  event => doSomething(event)
)

There's one situation where you'll need to adopt a little bit of type unsafety: when using Listenable to configure a Recognizeable instance for recognizing a custom gesture.

Recall that to use Listenable with Recognizeable, you should pass recognizeable as the Listenable constructor's type argument, and pass your Recognizeable instance's options to listenableOptions.recognizeable

const instance = new Listenable(
  'recognizeable',
  { recognizeable: { ... } }
)

There's a way to type-annotate your code so that your options.recognizeable object is fully type checked based on all the possible events that you want your Recognizeable instance to be able to handle, and type unsafety is minimized.

We won't cover all that information here, though. Instead, you should visit the Using with TypeScript section of the Recognizeable docs. Those docs give full information on not only how to set up a standalone Recognizeable instance, but more importantly, how to set up a fully type-safe Listenable instance that uses Recognizeable under the hood to recognize custom gestures.

API design compliance

Spec
Compliance status
Notes
Access functionality by constructing an instance
Constructor accepts two parameters: a piece of state, and an options object.
Constructor does not access the DOM
Takes the form of a JavaScript Object
State and methods are accessible through properties of the object
Methods always return the instance
Stores the constructor's state in a public getter named after the state's type
type
Has a public method you can use to set a new value for that public getter
setType
Has a setter for that getter so you can assign a new value directly
Any other public getters that should be set by you in some cases also have setters and set<Property> methods
none
Has at least one additional getter property that you can't (and shouldn't) set directly
status, active, recognizeable
Has one or more public methods that expose core functionality
listen, stop
Either has no side effects or has side effects that can be cleaned up with a stop method
stop
Uses the sentence template to decide what state type should be accepted by a constructor
"A type of event can be listened for."
Constructor does not accept options that only customize the behavior of public methods, it allows those options to be passed to the method itself as a parameter.
Named after its core action, proper-cased and suffixed with able

GrantableNavigateable

Edit doc on GitHub

ON THIS PAGE

ListenableExampleConstruct a Listenable instanceValid event typesListenable constructor optionsState and methodsHow to handle eventsHow to customize listen behaviorDefault values for target based on typeAvailable options for each typeHow to listen for custom gesturesUsing with TypeScriptAPI design compliance