ESL Event Listeners
Starting from the 4th release ESL has a built-in mechanism to work with DOM events. ESL event listeners have more control and advanced features than native DOM API. Besides, the ESLBaseElement
and the ESLMixinElement
have even more pre-built syntax sugar to make the consumer's code briefer.
One of the main advantages of ESL listeners is the extended control of subscriptions. All ESL listeners and their declarations are saved and associated with the host element. It means that ESL listeners can be subscribed or unsubscribed at any time in various ways. And most importantly, you do not need the original callback handler to do this.
Basic concepts
The ESL event listener module is based on the following major terms and classes:
The host
Everything that happens with ESL event listeners should be associated with a host
object. The host
is an object that "owns" (registers / deletes) the subscription.
By default, the host
is used as an EventTarget
(or an object implementing EventTarget
interface) to subscribe. But the host
object is not necessarily related to an EventTarget
. We have at least two options to change the target. First of all, you can define the target explicitly. Another way is to specify the default DOM target of the host object by providing a special $host
key (see ESLMixinElement
implementation).
The host
object is also used as a context to call the handler function of the subscription.
The handler
The handler
is a function that is used to process the subscription event. ESL declares a generic type to describe such functions - ESLListenerHandler
;
export type ESLListenerHandler<EType extends Event = Event> = (
event: EType
) => void;
The ESLEventListener
class and subscription
The subscriptions created by the ESL event listener module are instances of ESLEventListener
class. All active subscriptions are stored in a hidden property of the host
object.
ESLEventListener
has the following basic properties:
event
- event type that the subscription is listening to;handler
- reference for the function to call to handle the event (see The handler);host
- reference for the object that holds the subscription (see The host);target
- definition ofEventTarget
element (or stringTraversingQuery
to find it, see details inESLEventDesriptor
);selector
- CSS selector to use built-in event delegation;capture
- marker to use the capture phase of the DOM event life-cycle;passive
- marker to use passive (non-blocking) subscription of the native event (if supported);once
- marker to destroy the subscription after the first event catch.group
- auxiliary property to group subscriptions. Does not affect the subscription behavior. Can be used for filtering and bulk operations.
All of the ESLEventListener
instance fields are read-only; the subscription can't be changed once created.
The ESLEventListener
, as a class, describes the subscription behavior and contains static methods to create and manage subscriptions.
Descriptors (ESLEventDesriptor
, ESLEventDesriptorFn
)
The event listener Descriptor is an object to describe future subscriptions. The ESL event listeners module has a few special details regarding such objects.
A simple descriptor is an object that is passed to ESL event listener API to create a subscription. It contains almost the same set of keys as the ESLEventListener
instance.
In addition to that, ESL allows you to combine the ESLEventDesriptor
data with the handler function. ESLEventDesriptorFn
is a function handler that is decorated with the ESLEventDesriptor
properties.
Here is the list of supported keys of ESLEventDesriptor
:
-
event
keyType:
string | PropertyProvider<string>
Description: the event type for subscription. Can be provided as a string or via provider function that will be called right before the subscription.The event string (as a literal, or returned by
PropertyProvider
) can declare multiple event types separated by space. ESL will create a subscription (ESLEventListener
object) for each event type in this case. -
target
keyType:
string | EventTarget | EventTarget[] | PropertyProvider<string | EventTarget | EventTarget[]>
Default Value:host
object itself or$host
key of thehost
object
Description: the key to declare exact EventTarget for the subscription. In case thetarget
key is a string it is considered as aTraversingQuery
. The query finds a target relatively tohost | host.$host
object, or in bounds of the DOM tree if it is absolute. Thetarget
key also supports an exact reference forEventTarget
(s).⚠ Any
EventTarget
or even ESLSynteticEventTarget
(includingESLMediaQuery
) can be a target for listener API.⚠ See OOTB Extended Event Targets of ESL to know how to optimize handling of frequent events.
The
target
property can be declared viaPropertyProvider
as well. -
selector
keyType:
string | PropertyProvider<string>
Default Value:null
Description: the CSS selector to filter event targets for event delegation mechanism.⚠ If you want to get the currently delegated event target, you can access the
$delegate
key under the received event instance. In order to have access to$delegate
strictly typed use theDelegatedEvent<EventType>
type decorator.E.g.:
@listen({ event: 'click', selector: 'button' }) onClick(e: DelegatedEvent<MouseEvent> /* instead of MouseEvent */) { const delegate = e.$delegate; //instaead of e.target && e.target.closest('button'); ... }
Supports
PropertyProvider
to declare the computed value as well. -
condition
keyType:
bollean | PropertyProvider<boolean>
Default Value:true
Description: the function predicate or boolean flag to check if the subscription should be created. Resolves right before the subscription.Useful in combination with
@listen
decorator to declare subscriptions.class MyEl extends ESLBaseElement { @attr() enabled = true; @listen({event: 'click', condition: (that) => that.enabled}) onClick(e) {} attributeChangedCallback(name, oldValue, newValue) { if (name === 'enabled') { ESLEventUtils.unsubscribe(this, this.onClick); ESLEventUtils.subscribe(this, this.onClick); } } }
-
capture
keyType:
boolean
Default Value:false
Description: marker to use capturing phase of the DOM event to handle. -
passive
keyType:
boolean
Default Value:true
if the event type iswheel
,mousewheel
,touchstart
ortouchmove
Description: marker to use passive subscription to the native event.⚠ ESL uses passive subscription by default for
wheel
,mousewheel
,touchstart
,touchmove
events. You need to declarepassive
key explicitly to override this behavior. -
once
keyType:
boolean
Default Value:false
Description: marker to unsubscribe the listener after the first successful handling of the event. -
group
keyType:
string
Description: auxiliary property to group subscriptions. Does not affect the subscription behavior. Can be used for filtering and bulk operations.E.g.:
ESLEventUtils.subscribe(host, {event: 'click', group: 'group'}, handler1); ESLEventUtils.subscribe(host, {event: 'click', group: 'group'}, handler2); // ... ESLEventUtils.unsubscribe(host, {group: 'group'}); // Unsubscribes all subscriptions with the 'group' key
-
auto
key (forESLEventDesriptorFn
declaration only)Type:
boolean
Default Value:false
forESLEventUtils.initDescriptor
,true
for@listen
decorator Description: marker to make an auto-subscribable descriptor. See Automatic (collectable) descriptors. -
inherit
key (forESLEventDesriptorExt
only)Type:
boolean
Description: available in extended version ofESLEventDesriptor
that is used in the descriptor declaration API. Allows to inheritESLEventDesriptor
data from theESLEventDesriptorFn
from the prototype chain. SeeinitDescriptor
usages example.
Automatic (collectable) descriptors
Auto-collectable (or auto-subscribable) descriptors can be subscribed at once during the initialization of the host
object.
To make an ESLEventDesriptorFn
auto-collectable, the consumer should declare it with the auto
marker using ESLEventUtils.initDescriptor
or @listen
decorator.
⚠ ESLEventUtils.initDescriptor
(or @listen
) stores the auto-collectable descriptors in the internal collection on the host
.
The ESLBaseElment
and the ESLMixinElement
subscribes all auto-collectable descriptors in the connectedCallback
. See the usage of ESLEventUtils.subscibe
for more details.
PropertyProvider
for event
, selector
, or target
The descriptor declaration usually happens with the class declaration when the instance and its subscription do not exist. We might have a problem if we want to pass subscription parameters that depend on the instance.
To resolve such a case, the event
, selector
, and target
keys of ESL event listener API support PropertyProvider
mechanism:
type PropertyProvider<T> = (this: unknown, that: unknown) => T;
See examples in the ESLEventUtils.initDescriptor section.
Public API (ESLEventUtils
)
The units mentioned earlier are mostly implementation details of the module.
ESLEventUtils
is a facade for all ESL event listener module capabilities.
Here is the module Public API:
⚡ ESLEventUtils.subscribe
Creates and subscribes an ESLEventListener
.
- Subscribes all auto-collectable (subscribable) descriptors of the
host
object:ESLEventUtils.subscribe(host: object)
- Subscribes
handler
function to the DOM event declared byeventType
string:ESLEventUtils.subscribe(host: object, eventType: string, handler: ESLListenerHandler)
- Subscribes
handler
instance ofESLEventDescriptorFn
using embedded meta-information:ESLEventUtils.subscribe(host: object, handler: ESLEventDescriptorFn)
- Subscribes
handler
function usingESLEventDescriptor
:ESLEventUtils.subscribe(host: object, descriptor: ESLEventDescriptor, handler: ESLListenerHandler)
- Subscribes
handler
instance ofESLEventDescriptorFn
withESLEventDescriptor
overriding meta-data:ESLEventUtils.subscribe(host: object, descriptor: ESLEventDescriptor, handler: ESLEventDescriptorFn)
Parameters:
host
- host element to store subscription (event target by default);eventType
- string DOM event type;descriptor
- event description data (ESLEventDescriptor
);handler
- function callback handler or instance ofESLEventDescriptorFn
Examples:
ESLEventUtils.subscribe(host);
- subscribes all auto-subscriptions of thehost
;ESLEventUtils.subscribe(host, handlerFn);
- subscribeshandlerFn
method (decorated as anESLEventDescriptorFn
) to thehandlerFn.target
;ESLEventUtils.subscribe(host, 'click', handlerFn);
- subscribeshandlerFn
function with the passed event type;ESLEventUtils.subscribe(host, {event: 'scroll', target: window}, handlerFn);
- subscribeshandlerFn
function with the passed additional descriptor data.
⚡ ESLEventUtils.unsubscribe
Allows unsubscribing existing subscriptions.
unsubscribe(host: HTMLElement, ...criteria: ESLListenerCriteria[]): ESLEventListener[];
Parameters:
host
- host element to find subscriptions;criteria
- optional set of criteria to filter listeners to remove.
Examples:
ESLEventUtils.unsubscribe(host);
- unsubscribes everything bound to thehost
ESLEventUtils.unsubscribe(host, handlerFn);
- unsubscribes everything that is bound to thehost
and is handled by thehandlerFn
ESLEventUtils.unsubscribe(host, 'click');
- unsubscribes everything bound to thehost
and processingclick
eventESLEventUtils.unsubscribe(host, 'click', handlerFn);
- unsubscribes everything that is bound to thehost
, processingclick
event and is handled by thehandlerFn
- There can be any number of criteria.
⚡ ESLEventUtils.isEventDescriptor
Predicate to check if the passed argument is a type of ESLListenerDescriptorFn = ESLEventHandler & Partial<ESLEventDescriptor>
.
ESLEventUtils.isEventDescriptor(obj: any): obj is ESLListenerDescriptorFn;
⚡ ESLEventUtils.descriptors
Gathers descriptors from the passed object. Accepts criteria to filter the descriptors list.
ESLEventUtils.descriptors(host?: any): ESLListenerDescriptorFn[];
ESLEventUtils.descriptors(host?: any, ...criteria: ESLListenerDescriptorCriteria[]): ESLListenerDescriptorFn[];
Parameters:
host
- object to get auto-collectable descriptors from;
⚡ ESLEventUtils.getAutoDescriptors
ESLEventUtils.getAutoDescriptors
Gathers auto-subscribable (collectable) descriptors from the passed object.
Deprecated: prefer using ESLEventUtils.descriptors
with the {auto: true}
criteria. As the getAutoDescriptors
method is going to be removed in 6th release.
ESLEventUtils.getAutoDescriptors(host?: any): ESLListenerDescriptorFn[]
Parameters:
host
- object to get auto-collectable descriptors from;
⚡ ESLEventUtils.initDescriptor
Decorates the passed key of the host object as ESLEventDescriptorFn
ESLEventUtils.initDescriptor<T extends object>(
host: T,
key: keyof T & string,
desc: ESLEventDescriptorExt
): ESLEventDescriptorFn;
Parameters:
host
- host object holder of decorated function;key
- key of thehost
object that contains a function to decorate;desc
-ESLEventDescriptor
(extended) meta information to describe future subscriptions.
The extended ESLEventDescriptor
information allows passing inherit
marker to create a new descriptor instance based on the descriptor declared in the prototype chain for the same key.
⚠ If such a key is not found, a ReferenceError
will be thrown.
Example:
class MyElement {
onEvent() {}
}
ESLEventUtils.initDescriptor(MyElement.prototype, 'onEvent', {event: 'event'});
@listen
decorator
The @listen
decorator (available under esl-utils/decorators
) is syntax sugar above ESLEventUtils.initDescriptor
method. It allows you to declare class methods as an ESLEventDescriptorFn
using TS experimentalDecorators
feature.
Listeners described by @listen
are auto-subscribable if they are not inherited and not declared as manual explicitly. In case of inheritance the auto
marker will be inherited from the parent descriptor.
Example:
class MyEl extends ESLBaseElement {
private event: string;
private selector: string;
// Shortcut with just an event type
@listen('click')
onClick() {}
// Shortcut with event type declared by PropertyProvider
@listen((that: MyEl) => that.event)
onEventProvided() {}
// Full list of options is available
@listen({event: 'click', target: 'body', capture: true})
onBodyClick(e) {}
// Property Providers example
@listen({
event: (that: MyEl) => that.event,
seletor: (that: MyEl) => that.selector
})
onEventProvidedExt(e) {}
// Will not subscribe authomatically
@listen({event: 'click', auto: false})
onClickManual(e) {}
}
⚡ ESLEventUtils.listeners
Gathers listeners currently subscribed to the passed host
object.
ESLEventUtils.listeners(host: object, ...criteria: ESLListenerCriteria[]): ESLEventListener[];
Parameters:
host
- object that stores and relates to the handlers;criteria
- optional set of criteria to filter the listeners list.
⚡ ESLEventUtils.dispatch
Dispatches custom DOM events. The dispatched event is bubbling and cancelable by default.
ESLEventUtils.dispatch(
el: EventTarget,
eventName: string,
eventInit?: CustomEventInit
): boolean;
Parameters:
el
-EventTarget
to dispatch event;eventName
- name of the event to dispatch;eventInit
- object that specifies characteristics of the event.
Listeners Full Showcase Example
class TestCases {
bind() {
// Subcribes all auto descriptors (onEventAutoDescSugar and onEventAutoDesc)
ESLEventUtils.subscribe(this);
// Subscribes onEventManualFn on click
ESLEventUtils.subscribe(this, 'click', this.onEventManualFn);
// Subscribes onEventManualFn on window resize
ESLEventUtils.subscribe(this, {event: 'resize', target: window}, this.onEventManualFn);
// Subscribes onEventManualDesc using embeded information
ESLEventUtils.subscribe(this, this.onEventManualDesc);
// Subscribes onEventManualDesc using merged embeded and passed information
ESLEventUtils.subscribe(this, {target: window}, this.onEventManualDesc);
}
unbind() {
// Unsubcribes all subscriptions
ESLEventUtils.unsubscribe(this);
// Unsubcribes just onEventAutoDesc
ESLEventUtils.unsubscribe(this, this.onEventAutoDesc);
}
@listen('event')
onEventAutoDescSugar() {}
onEventAutoDesc() {}
onEventManualFn() {}
onEventManualDesc() {}
}
ESLEventUtils.initDescriptor(TestCases.prototype, 'onEventAutoDesc', {event: 'event', auto: true});
ESLEventUtils.initDescriptor(TestCases.prototype, 'onEventManualDesc', {event: 'event'});
Extended EventTarget
s and standard optimizations beta
⚡ ESLDecoratedEventTarget.for
In cases where the original event of the target happens too frequently to be handled every time, it might be helpful to limit its processing. In purpose to do that ESL allows the creation of decorated EventTargets
. The decorated target will process the original target events dispatching with the passed async call decoration function (such as debounce or throttle).
The ESLDecoratedEventTarget.for
creates an instance that decorates passed original EventTarget
event emitting. The instances of ESLDecoratedEventTarget
are lazy and do not subscribe to the original event until they have their own subscriptions of the same event type.
⚠ Note ESLDecoratedEventTarget.for
method is cached, so created instances will be reused if the inner cache does not refuse additional arguments of the decorator. The cache does not handle multiple and non-primitive arguments.
ESLDecoratedEventTarget.for(
target: EventTarget,
decorator: (fn: EventListener, ...args: any[]) => EventListener,
...args: any[]
): ESLDecoratedEventTarget;
Parameters:
target
- originalEventTarget
to consume events;decorator
- decoration function to decorate original targetEventListener
s;args
- optional arguments to pass todecorator
.
Example:
class Component {
@listen({
event: 'scroll',
target: ESLDecoratedEventTarget.for(window, throttle)
})
onScroll() {}
}
Sharing of the decorated targets
As was mentioned above, the method ESLDecoratedEventTarget.for
works with a cache for simple cases. But in some cases, we might be interested in creating wrappers with a complex param, or we want to limit params usage across the project.
It might sound obvious, but there are no restrictions on sharing exact instances instead of using the method cache.
// shared-event-targets.ts
export const DEBOUNCED_WINDOW = ESLDecoratedEventTarget.for(window, debounce, 1000);
// module.ts
class Component {
@listen({event: 'resize', target: DEBOUNCED_WINDOW})
onResize() {}
}
Optimize window.resize
handling with debouncing
import {debounce} from '.../debounce';
ESLEventUtils.subscribe(host, {
event: 'resize',
target: /* instead just window */ ESLDecoratedEventTarget.for(window, debounce, 250)
}, onResizeDebounced);
The sample above allows you to reuse debounced by 250 milliseconds version of the window, to receive fewer resize
events (same as any other event types observed on debounced window version)
Optimize window.scroll
handling with throttling
import {throttle} from '.../throttle';
ESLEventUtils.subscribe(host, {
event: 'scroll',
target: /* instead just window */ ESLDecoratedEventTarget.for(window, throttle, 250)
}, onScrollThrottled);
The sample above allows you to reuse throttled by 250 milliseconds version of the window, to receive no more than one event per 250 milliseconds scroll
events (same as any other event types observed on debounced window version)
⚡ ESLResizeObserverTarget.for
When you deal with responsive interfaces, you might need to observe an element resizes instead of responding to the whole window change. There is a tool for this in the native DOM API - `ResizeObserver'. The only problem is that it does not use events, while in practice, we work with it in the same way.
ESLResizeObserverTarget.for
creates cached ResizeObserver
adaptation to EventTarget
(ESLResizeObserverTarget
) that allows you to get resize
events when the observed element changes its size.
ESLResizeObserverTarget.for(el: Element): ESLResizeObserverTarget;
Parameters:
el
-Element
to observe size changes.
ESLResizeObserverTarget
creates itself once for an observed object with a weak reference-based cache. So any way of creating ESLResizeObserverTarget
will always produce the same instance.
ESLResizeObserverTarget.for(el) /**always*/ === ESLResizeObserverTarget.for(el)
So there is no reason to cache it manually.
Usage example:
ESLEventUtils.subscribe(host, {
event: 'resize',
target: ESLResizeObserverTarget.for(el)
}, onResize);
// or
ESLEventUtils.subscribe(host, {
event: 'resize',
target: (host) => ESLResizeObserverTarget.for(host.el)
}, onResize);
⚡ ESLSwipeGestureTarget.for
new
ESLSwipeGestureTarget.for
is a simple and easy-to-use way to listen for swipe events on any element.
ESLSwipeGestureTarget.for
creates a synthetic target that produces swipe
events. It detects pointerdown
and pointerup
events and based on the distance (threshold
) between start and end points and time (timeout
) between pointerdown
and pointerup
events, triggers swipe
event on the target element.
ESLSwipeGestureTarget.for(el: Element, settings?: ESLSwipeGestureSetting): ESLSwipeGestureTarget;
Parameters:
el
-Element
to listen for swipe events on.settings
- optional settings (ESLSwipeGestureSetting
)
Usage example:
ESLEventUtils.subscribe(host, {
event: 'swipe',
target: ESLSwipeGestureTarget.for(el)
}, onSwipe);
// or
ESLEventUtils.subscribe(host, {
event: 'swipe',
target: (host) => ESLSwipeGestureTarget.for(host.el, {
threshold: '30px',
timeout: 1000
})
}, onSwipe);
⚡ ESLWheelTarget.for
new
ESLWheelTarget.for
is a simple way to listen for 'inert' (long wheel) scrolls events on any element. This utility detects wheel
events, and based on the total amount (distance) of wheel
events and time (timeout
) between the first and the last events, it triggers longwheel
event on the target element.
ESLWheelTarget.for(el: Element, settings?: ESLWheelTargetSetting): ESLWheelTarget;
Parameters:
el
-Element
to listen for long wheel eventssettings
- optional settings (ESLWheelTargetSetting
)
The ESLWheelTargetSetting
configuration includes these optional attributes:
distance
- the minimum distance to accept as a long scroll in pixels (400 by default)timeout
- the maximum duration of the wheel events to consider it inertial in milliseconds (100 by default)
Usage example:
ESLEventUtils.subscribe(host, {
event: 'longwheel',
target: ESLWheelTarget.for(el)
}, onWheel);
// or
ESLEventUtils.subscribe(host, {
event: 'longwheel',
target: (host) => ESLWheelTarget.for(host.el, {
threshold: 30,
timeout: 1000
})
}, onWheel);
⚡ ESLIntersectionTarget.for
new
ESLIntersectionTarget.for
is a way to listen for intersections using Intersection Observer API but in an EventTarget way.
ESLIntersectionTarget.for
creates a synthetic target that produces intersection
events. It detects intersections by creating IntersectionObserver
instance, created using passed settings: IntersectionObserverInit
.
Note: ESLIntersectionTarget
does not share IntersectionObserver
instances unlike caching capabilities of adapters mentioned above.
ESLIntersectionTarget.for(el: Element | Element[], settings?: IntersectionObserverInit): ESLIntersectionTarget;
Parameters:
el
-Element
orElement[]
to listen for intersection events on;settings
- optional settings (ESLIntersectionSetting
)
Event API: Throws ESLIntersectionEvent
that implements IntersectionObserverEntry
original interface.
Embedded behavior of ESLBaseElement
/ ESLMixinElement
Shortcuts
All the inheritors of ESLBaseElement
and ESLMixinElement
contain the short aliases for some ESLEventUtils
methods. The host parameter of the shortcut methods is always targeting the current element/mixin.
$$on
~ESLEventUtils.subscribe(this, ...)
$$off
~ESLEventUtils.unsubscribe(this, ...)
$$fire
~ESLEventUtils.dispatch(this, ...)
Example:
this.$$on('click', this.onClick); // ESLEventUtils.subscribe(this, 'click', this.onClick)
this.$$off('click'); // ESLEventUtils.unsubscribe(this, 'click')
Auto-subscription / Auto-unbinding
All the inheritors of ESLBaseElement
and ESLMixinElement
automatically subscribe to all declared auto-subscribable descriptors of their prototype chain.
They also unsubscribe all own listeners attached via ESL automatically on disconnectedCallback
.
The following short snippet of code describes a listener that will automatically subscribe and unsubscribe on connected/disconnected callback inside ESLBaseElement
:
class MyEl extends ESLBaseElement {
// connectedCallback() {
// super.connectedCallback(); // - already contains ESLEventUtils.subscribe(this) call
// }
// Will be subscribed automatically on connectedCallback and unsubscribed on disconnectedCallback
@listen('click')
onClick(e) {
//...
}
}
You can manage the subscription manually and link the whole meta information or part of it with the handler itself
class MyEl extends ESLBaseElement {
@listen({event: 'click', auto: false}) // Won`t be subscribed automatically
onClick(e) {
// ...
}
myMethod() {
this.$$on(this.onClick); // Manual subscription
this.$$on({target: 'body'}, this.onClick); // Manual subscription with parameters (will be merged)
this.$$off(this.onClick); // Unsubscribes this.onClick method
}
}