Vinyl uses a statically-typed observer pattern. Event hosts describe the events they emit using an event map type. Observers may subscribe to those events, and unsubscribe using the returned unsubscribe handles.
An event host is not tied to the DOM, it may be used anywhere an observer pattern is needed and will work in both Node and Browser environments.
To subscribe to an event from an event host, use the on function, which
returns a function that, when invoked, removes the listener.
import { EventHostImpl } from '@amzn/vinyl-util'
export type CountEvent = {
readonly value: number
}
export type CounterEventMap = {
readonly count: CountEvent
}
export class Counter extends EventHostImpl<CounterEventMap> {
private _count = 0
increment() {
this._count++
this.dispatch('count', { value: this._count })
}
}
const counter = new Counter()
const sub = counter.on('count', (event) => {
console.log('count event:', event.value)
})
counter.increment() // count event: 1
counter.increment() // count event: 2
counter.increment() // count event: 3
sub() // remove listener
counter.increment() // event not observed
const counter = new Counter()
counter.on(
'count',
(event) => {
console.log('count event:', event.count)
},
{ once: true }
)
counter.increment() // count event: 1
counter.increment() // event not observed
When typing interfaces, ReadonlyEventHost<T> may be used to expose a readonly
interface to observers. This allows event maps to have correct variance and
enable TypeScript to ensure events are not incorrectly dispatched from outside
systems.
When an event host is disposed, all listeners will be removed. To ensure good garbage collection, follow an ownership principle. When adding listeners to a provided dependency, added listeners must be paired with an unsubscribe.
EventTarget objects may be wrapped using DomEventHost. Using DomEventHost
allows applications to use a consistent observer pattern, shared utilities, and
easier cleanup.
TypeScript provides event map types that may be used to statically type an EventTarget's events.
import { DomEventHost } from '@amzn/vinyl-util'
const div = document.querySelector('div')
const divEvents = new DomEventHost<HTMLElementEventMap>(div)
divEvents.on('click', (event) => console.log('clicked'))
When subscribing to a DomEventHost, additional options are available such as 'passive', or 'capture'.
divEvents.on('wheel', (event) => console.log('wheel'), {
passive: true,
capture: true,
})
Note: The event option 'signal' is not supported, to unsubscribe to many handlers at once, create a disposer.
E.g.
import { createDisposer } from '@amzn/vinyl-util'
const { add, dispose } = createDisposer()
add(divEvents.on('click', (event) => console.log('click')))
add(divEvents.on('wheel', (event) => console.log('wheel')))
dispose() // removes all handlers added
To redispatch events, use the redispatchEvents utility method.
Example:
import { EventHostImpl } from '@amzn/vinyl-util'
class MyWrapper extends EventHostImpl<HTMLElementEventMap> {
private readonly divEvents = new DomEventHost<HTMLElementEventMap>(div)
constructor() {
super()
redispatchEvents(this, this.divEvents, [
'click',
'mouseMove',
'touchStart',
'touchEnd',
])
}
}
To extend an EventHost implementation, adding new events to a subclass, certain functions on EventHostImpl must be overridden.
note This scenario is in need of improvement. To ensure full static checking for events and event types, method overrides when extending EventHostImpl is necessary, however the ergonomics can be improved.
import type { EmptyObject, EventHost } from '@amzn/vinyl-util'
export type CounterWithResetEventMap = CounterEventMap & {
readonly reset: EmptyObject
}
class CounterWithReset
extends Counter
implements EventHost<CounterWithResetEventMap>
{
hasListeners(type: keyof CounterWithResetEventMap): boolean {
return super.hasListeners(type as any)
}
on<K extends keyof CounterWithResetEventMap>(
type: K,
handler: EventHandler<CounterWithResetEventMap[K]>,
options?: SignalOptions
): Unsubscribe {
return super.on(type as any, handler, options)
}
dispatch<K extends keyof CounterWithResetEventMap>(
type: K,
event: CounterWithResetEventMap[K]
) {
super.dispatch(type as any, event)
}
}