Recognizeable
Updated on August 22, 2024Source codeTests
Recognizeable is a class that enriches a sequence of DOM events, allowing it to:
- Recognize itself as something more abstract, like a "swipe" gesture, a keychord, etc.
- Store metadata about itself
- Store a status (
'ready',recognizing,recognized, ordenied)
Construct a Recognizeable instance
The Recognizeable constructor accepts two parameters:
sequencePasses the event sequence (Array) that will be made recognizable.
In all intended use cases, Listenable will be constructing the Recognizeable instance for you, and it will pass an empty array here.
optionsPasses options for the Recognizeable instance. See the Recognizeable constructor options section for more guidance.
This is where Listenable delivers the options object you pass to Listenable's recognizeable option.
Recognizeable constructor options
effects0The object that contains the side effect functions that help recognize your custom sequence.
effects can also be a function that returns an array of tuples that define your effects, but this format is only necessary if you want TypeScript support.
See the How to format effects section for more guidance on formatting the effects object.
maxSequenceLengthtruetrueIndicates the number of events that should be stored in the sequence array. When a new event is received, Recognizeable removes the first event in the sequence if its length would otherwise exceed maxSequenceLength.
Set maxSequenceLength to true if you don't want to limit the number of events that are stored.
How to format effects
effects is an object, and its properties can be any valid Listenable event type.
const instance = new Recognizeable(
[],
{
effects: {
click: ...,
intersect: ...,
message: ...,
}
}
)
The value for each property should be an "effect": a function designed to handle incoming items in your sequence. Your Recognizeable instance will pass two arguments those functions:
- The most recent item that was added to the
sequence - The Effect API.
const instance = new Recognizeable(
[],
{
effects: {
click: (mouseEvent, effectApi) => {
...
},
intersect: (intersectionObserverEntry, effectApi) => {
...
},
message: (messageEvent, effectApi) => {
...
},
}
}
)
Here's a breakdown of the Effect API:
getStatus()Gets the current status from the Recognizeable instance.
See the Access state and methods section for more info about status.
Recognizeable instance's statusgetMetadata()Gets the current metadata from the Recognizeable instance.
This metadata object is mutable, and any changes to it will directly affect the Recognizeable instance's metadata property.
See the Access state and methods section for more info about metadata.
Recognizeable instance's metadata objectsetMetadata(metadata)metadata object with a new one.metadatarecognized()Sets the Recognizeable instance's status to recognized, and updates the sequence to include the most recent event.
You should only call this function after the information you've gathered from events and stored in metadata proves that your custom gesture has occurred.
denied()Sets the Recognizeable instance's status to denied, resets the instance's metadata to an empty object, and resets the instance's sequence to an empty array.
You should only call this function after the information you've gathered from events and stored in metadata proves that your custom gesture can't possibly occur, and everything should reset so you can start recognizing again with a clean slate.
getSequence()Gets the current sequence from the Recognizeable instance (including the most recent sequenceItem at the end).
See the Access state and methods section for more info about sequence.
Recognizeable instance's sequenceYou can use that API to extract information from each item in the sequence, store it in metadata, and decide when the sequence has been recognized.
const instance = new Recognizeable(
[],
{
effects: {
click: (event, effectApi) => {
const { getMetadata, recognized } = effectApi,
metadata = getMetadata(),
{ clientX, clientY } = event
metadata.lastClickPosition = {
x: clientX,
y: clientY,
}
if (metadata.lastClickPosition.x === 420 && metadata.lastClickPosition.y === 420) {
recognized()
}
},
intersect: effectApi => ...,
message: effectApi => ...,
}
}
)
That's a lot of information to throw at you! If this is your first read through, you should be confused at this point. Don't sweat it—later on, the Effect workflow section should give more clarity.
State and methods
sequenceThe sequence array passed to the constructor.
If you assign a value directly to sequence, a setter will pass the new value to setSequence.
statusRecognizeable instance. See the How methods affect status section for more information.metadataeffects option.setSequence(sequence)sequenceRecognizeable instancerecognize(sequenceItem)An event, array of observer entries, etc.
Recognizeable instanceEffect workflow
Now that you've read about Recognizeable's state and methods, and you've finished drowning in the Effect API breakdown, it's time to learn more about Recognizeable's effect workflow.
Here's what the workflow looks like:
- A sequence item gets passed to the
recognizemethod. - Internally, the
recognizemethod deduces the type of that sequence item. - The
recognizemethod looks through itseffects(passed to the constructor option) to finds the effect that matches the deduced event type. recognizecalls that effect function, passing the Effect API (which includes the original sequence item).- Your effect function should extract some information from the Effect API. In most cases, this information will be extracted from
effectApi.sequenceItem. For example: at whatxandycoordinates did amousedowntake place? Which keyboard key was just released? According to the latestResizeObserverentry, what's the new width of a certain element? - Your effect function can use the Effect API's
getMetadatafunction to access theRecognizeableinstance'smetadataobject. To store your extracted information, you can freely assign values to the properties of that object. - Based on all of the information you've gathered, your effect should make a decision:
- Has the custom gesture or sequence been recognized? If so, call the
recognizedfunction.Recognizeablewill update its status torecognized. - Still not sure, and need to wait for more events? Do nothing—
Recognizeablewill keep its status asrecognizing. - Final option: did something happen that makes your custom gesture or sequence impossible (e.g. a
mouseupevent when you're trying to recognize a drag/pan gesture)? If so, call thedeniedfunction to explicitly deny the sequence.Recognizeablewill update its status todenied.
- Has the custom gesture or sequence been recognized? If so, call the
Using with TypeScript
Recognizeable is designed to provide robust autocomplete and type checking, especially inside your effects functions, on the instance's recognize method, in the instance's metadata property.
Let's dive right into an annotated code example to see how TypeScript support works:
// Pass a union type to `Recognizeable`'s first generic type
// to tell the instance what types of events it's allowed to
// recognize. Any valid `Listenable` event type is supported.
//
// Use the second generic type to define the shape of the
// instance's `metadata` property.
type MyTypes = 'mousedown' | 'intersect' | 'message'
type MyMetadata = {
x: number,
y: number,
}
const instance = new Recognizeable<MyTypes, MyMetadata>(
// This sequence will automatically have a type of:
// (MouseEvent | IntersectionObserverEntry[] | MessageEvent)[]
[],
{
effects: {
mousedown: (sequenceItem, effectApi) => {
// `sequenceItem` is correctly type checked and
// autocompleted as a MouseEvent.
console.log(sequenceItem)
const metadata = effectApi.getMetadata()
// TypeScript knows the shape of `metadata` here. It will
// autocomplete `metadata` and allow you to do this
// assignment.
metadata.x = sequenceItem.clientX
metadata.y = sequenceItem.clientY
},
intersect: (sequenceItem, effectApi) => {
// `sequenceItem` is correctly type checked and
// autocompleted as an array of Intersection Observer
// entries.
console.log(sequenceItem)
},
message: (sequenceItem, effectApi) => {
// `sequenceItem` is correctly type checked and
// autocompleted as a MessageEvent.
console.log(sequenceItem)
},
// TypeScript will throw a type error here! `pointerdown`
// is not included in the union we passed to the instance's
// first generic type.
pointerdown: (sequenceItem, effectApi) => {
console.log(sequenceItem)
},
}
}
)
// TypeScript will allow you to pass MouseEvents, MessageEvents,
// and IntersectionObserverEntry[] arrays to the `recognize` method.
instance.recognize(new MouseEvent('click'))
instance.recognize(new MessageEvent('message'))
// TypeScript will not allow you to pass other events.
// This will throw a type error!
instance.recognize(new TouchEvent('touchstart'))
// TypeScript knows the shape of `metadata`, and will type check
// this assignment.
const myVariable: number = instance.metadata.x
Let's review a few key details from that code.
The Recognizeable constructor accepts two generic types. Use the first type to define which valid Listenable event types can be recognized by the instance. Use the second type to define the shape of Recognizeable.metadata.
TypeScript reads the keys of options.effects to provide great type checking for each side effect individually.
TypeScript won't allow you to recognize unsupported events. If options.effects isn't prepared to handle a given effect, TypeScript won't let you pass it in.
Finally, be aware that all of these same principles apply when you're using Recognizeable with Listenable, which is what you'll be doing in pretty much every use case.
Here's an annotated code example of using Recognizeable via Listenable:
// Pass a union type to Listenable's first generic type
// to tell the instance what types of events it's allowed to
// listen for.
//
// Use Listenable's second generic type to define the shape of the
// `metadata` for the Recognizeable instance that Listenable
// will construct internally.
type MyTypes = 'mousedown' | 'intersect' | 'message'
type MyMetadata = {
x: number,
y: number,
}
const instance = new Listenable<MyTypes, MyMetadata>(
// Assert that the string 'recognizeable' is compatible with your
// type union. This of course is not type safe, but it's a
// small tradeoff that was made to simplify Listenable's inner
// workings and public-facing API.
'recognizeable' as MyTypes,
{
// Use options.recognizeable to pass your Recognizeable options
recognizeable: {
effects: {
// Listenable and Recognizeable work together to provide
// type checking for each individual side effect.
mousedown: ...,
intersect: ...,
message: ...,
}
}
}
)
instance.listen(() => {
// In your Listenable.listen callbacks, you can access
// Recognizeable metadata via your Listenable instance's
// `recognizeable` property.
//
// Listenable.recognizeable.metadata will be aware of
// the shape of your Recognizeable metadata, and it will
// type check and autocomplete accordingly.
const x: number = instance.recognizeable.metadata.x
})
Again, let's review a few key details from that code.
The Listenable constructor accepts two generic types. Use the first type to define which valid Listenable event types can be listened to by the instance. These are also the types that the internal Recognizeable instance will be able to recognize. Use the second type to define the shape of Listenable.recognizeable.metadata.
TypeScript reads the keys of options.recognizeable.effects to provide great type checking for each side effect individually.
Access listenableInstance.recognizeable.metadata from inside a listen callback to get type-safe metadata about the gesture you're listening for.
API design compliance
options object.sequencesetSequenceset<Property> methodsstatus, metadatarecognizestop methodable