on
Updated on September 7, 2024Source codeTests
on
is a function that manages all kinds of event listeners, adding them when a Vue component is mounted, and automatically removing them up when the component is destroyed.
Usage
To listen for events, call the on
function, which requires two parameters: the element, list of elements, or Plane
of elements you're listening on, and the side effects you want to perform.
import { on } from '@baleada/vue-features'
export function myCompositionFunction (...) {
on(elementOrListOrPlane, effects)
}
Here's a breakdown of the required parameters:
elementOrListOrPlane
A reactive reference to the DOM element or elements you're listening on.
elementOrListOrPlane
can be one of the following types:
- HTMLElement
- Array of HTMLElements
Plane
of HTMLElements- Reactive reference to any of the above types
See the How to format effects section for more guidance on using your event listener/observer callback/etc. to access specific elements in a reactive array or reactive Plane
.
effects
An object describing side effects, performed via callback functions, that you want to run when events are fired on your element, list of elements, or Plane
of elements.
See the How to format the effects object section for more guidance on usage.
Also, see the Valid event types section of the Listenable
docs for more guidance on how to listen for specific keyboard shortcuts, resized elements, custom gestures, and much more.
Finally, note that effects
has some awesome TypeScript features to support type safety and autocomplete in your callbacks. You can learn more about that in the How to write type-safe effects section
How to format the effects object
The effects
parameter is an object. The properties of that object must be valid Listenable
event types:
import { on } from '@baleada/vue-features'
export function myCompositionFunction (...) {
on(
myElement,
{
click: ...,
keydown: ...,
intersect: ...,
recognizeable: ..., // For custom gestures
}
)
}
How to format effects
There are several different ways to format the effects that on
will add to your elements, and perform each time their corresponding event occurs.
The simplest type of listener is a plain callback function, exactly like the one you might pass to addEventListener
or IntersectionObserver.observe
:
import { on } from '@baleada/vue-features'
on(
...
{
click: mouseEvent => { ... },
keydown: keyboardEvent => { ... },
intersect: intersectionObserverEntries => { ... },
}
)
But in some situations, you'll need to configure the listener more precisely. For example, you'll need to pass options to addEventListener
or the IntersectionObserver
constructor (both used under the hood by Listenable
), or you might want to configure the underlying Listenable
instance to listen for custom gestures.
In these cases, you can pass an effect config as the key's value. The effect config is an object with two properties: createEffect
and options
.
The value of createEffect
should be a callback function that returns a function with side effects:
import { on } from '@baleada/vue-features'
on(
...,
{
// Each `createEffect` returns the function that should
// be called each time an event happens, or an Observer
// adds an entry, etc.
click: {
createEffect: (...) => mouseEvent => { ... },
},
keydown: {
createEffect: (...) => keyboardEvent => { ... },
},
intersect: {
createEffect: (...) => intersectionObserverEntries => { ... },
},
}
)
The createEffect
function receives two arguments: the index
(Number) of a given element in your reactive array of elements, and an object with some useful values.
Here's a breakdown of that object:
off
A function you can call with no arguments to immediately stop listening for the event on the current element
. The event listener (or observer, etc.) will be cleaned up and will be inactive for the rest of the component's lifecycle OR until Vue detects a change to the DOM target or array of DOM elements you passed as the elementOrListOrPlane
parameter.
To implement the once
feature of Vue, where an event listener only runs once, you can call off
at the end of your listener.
listenable
The Listenable
instance used by on
under the hood to set up listeners, connect observers, parse keycombos, etc.
listenable
is primarily useful when you're using on
with Recognizeable
to listen for custom gestures—you can access Recognizeable
's metadata via listenable.recognizeable
.
Again, the createEffect
, given this object as an argument, should return an event listener.
The other key of the effect config is options
, which is an object. Here's a breakdown of the options
object:
listenable
{}
The optional second parameter of the Listenable
constructor.
You can use effectConfig.options.listenable
to configure custom gesture recognition powered by Recognizeable
.
listen
{}
The optional second parameter of Listenable.listen
method.
You can use effectConfig.options.listenable
to pass things like addEventListener
options or IntersectionObserver
constructor options.
import { mousedrag } from '@baleada/recognizeable-effects'
export function useGrid (...) {
...
on(
root.element,
{
// To listen to a custom gesture powered by `Recognizeable`,
// use `recognizeable` as the effect key name.
recognizeable: {
// `createEffect` returns the event handler, which will get
// called as soon as the `mousedrag` gesture is recognized.
createEffect: () => (event, { is }) => {
// Do some magic to figure out which grid cells get added
// to the current selection when the user clicks and drags
// across the grid.
},
// `options.listenable` passes the optional second parameter
// of the underlying `Listenable` instance. In this case,
// that feature is used to configure the `mousedrag` gesture
// with `Listenable`.
options: {
listenable: {
recognizeable: {
effects: mousedrag(...)
},
},
},
}),
}
)
...
}
And here's an example of use useVisibility
uses an effect config to configure the underlying IntersectionObserver
instance that tracks element visibility.
export function useVisibility (element, options) {
...
on(
element,
{
// When `on` and its underlying `Listenable` instance see
// the `intersect` effect name, they know to use an
// `IntersectionObserver` instance under the hood.
intersect: {
// `createEffect` returns the callback that should run
// when the `IntersectionObserver` adds a new entry.
createEffect: () => entries => {
// Store relevant entry data as reactive state
},
// `options.listen` is used to pass `IntersectionObserver`
// constructor options through to the underlying instance.
options: {
listen: {
observer: options.observer || {},
}
}
}
}
)
...
}
How to write type-safe effects
Thanks to some truly joy-sparking TypeScript tactics, type safety works out of the box for almost every use case.
import { on } from '@baleada/vue-features'
on(
...
{
click: event => {
// `event` is type-checked and autocompleted as a `MouseEvent`
},
keydown: event => {
// `event` is type-checked and autocompleted as a `KeyboardEvent`
},
intersect: entries => {
// `event` is type-checked and autocompleted as an array
// of `IntersectionObserverEntry` objects.
},
}
)
The one exception to this rule is custom gestures powered by Recognizeable
. To set those up, you use recognizeable
as the effect key:
import { on } from '@baleada/vue-features'
on(
...
{
recognizeable: event => { ... },
}
)
But since Recognizeable
instances are designed to collect and analyze pretty much any kind of event, including MouseEvent
, KeyboardEvent
, IntersectionObserverEntry
, MutationRecord
, etc., it's impossible to infer types automatically.
The solution is the defineRecognizeableEffect
type helper from Baleada Features.
There are three basic steps to using defineRecognizeableEffect
:
- Pass in generic types to support type safety in your event callback (which runs when the gesture is recognized).
- Pass an effect config object as the first and only parameter of
defineRecognizeableEffect
. ThecreateEffect
callback will type-check and autocomplete its arguments based on the generic types you passed todefineRecognizeableEffect
. - Spread the return value of
defineRecognizeableEffect
into youreffects
parameter.
Let's take a look at the basic pattern, and then we'll see a real world example.
import { on, defineRecognizeableEffect } from '@baleada/vue-features'
on(
element,
{
// We spread the return value of `defineRecognizeableEffect`
// into our `effects` parameter. We'll also use the function's
// generic types to support type safety for our callback function.
//
// `defineRecognizeableEffect`'s first and only parameter is an
// effect config object.
...defineRecognizeableEffect<...>({
// Enjoy type safety in `createEffect`
createEffect: () => event => { ... },
// For `options`, we'll use `options.listenable` to configure
// `Recognizeable` to recognize a custom gesture.
options: {
listenable: {
recognizeable: {
effects: { ... }
}
},
}
}),
}
)
Earlier in this guide, we looked at a simplified example of how useGrid
uses an effect config object to set up a Recognizeable
-powered listener for the mousedrag
gesture. In the source code, that listener is actually implemented with defineRecognizeableEffect
for type safety. Here's roughly how it works:
import {
mousedrag,
MousedragTypes,
MousedragMetadata
} from '@baleada/recognizeable-effects'
import { on, defineRecognizeableEffect } from '@baleada/vue-features'
export function useGrid (...) {
...
on(
element,
{
// Generic types for the target element, `mousedrag` event
// types (mousedown, mouseleave, and mouseup) and `mousedrag`
// metadata are passed in.
...defineRecognizeableEffect<
typeof element,
MousedragTypes,
MousedragMetadata
>({
createEffect: () => event => {
// `event` is type-checked and autocompleted as a
// `MouseEvent`, inferred from the `MousedragTypes`
// type.
//
// MousedragTypes -> 'mousedown' | 'mouseleave' | 'mouseup'
},
options: {
listenable: {
recognizeable: {
// `useGrid` calls the `mousedrag` function to create
// a suite of event listeners that will captured
// various mouse events and store their metadata
// for later analysis.
effects: mousedrag(...)
},
},
},
}),
}
)
...
}