Skip to content

Events hooks

Five naming conventions bind a method to an event source. All five are bound for the mount cycle and removed by $unmount().

Method nameListens toPayload
on<Event>the component's own elementthe raw event
on<Ref><Event>a declared ref, delegated{ event, target, index }
on<Child><Event>a component in config.components, delegated{ event, target, payload }
onWindow<Event>window{ event, target }
onDocument<Event>document{ event, target }

Resolution order

onWindow and onDocument are reserved prefixes and match first. After them a name is resolved children-first, then refs.

onWindowResize binds to window even in a component whose config.components holds a Window. To reach a child with that name, use @on('Window', 'resize').

The rule is about method names only. onClick and onDocumentClick are different names and both can exist on one component; a click on the element fires both.

on<Event>

js
import { 
Base
} from '@studiometa/js-toolkit';
class
Toggle
extends
Base
{
static
config
= {
name
: 'Toggle' };
onClick
(
event
) {
event
.preventDefault();
} }

The event name is the method name after on, in lower case: onPointerDown is pointerdown.

on<Ref><Event>

ts
import { 
Base
, type
RefEvent
} from '@studiometa/js-toolkit';
class
Tabs
extends
Base
{
static
config
= {
name
: 'Tabs',
refs
: ['tabs[]'] };
onTabsClick
({
event
,
target
,
index
}:
RefEvent
<HTMLButtonElement>) {
console
.
log
(
index
,
target
.
textContent
);
} }
ts
interface RefEvent<T extends HTMLElement = HTMLElement> {
  event: Event;
  target: T;
  index: number;
}

target is the ref element the handler matched, not event.target. index is the position in a list ref, or 0 for a single ref.

The ref is named as it is declared: onTabsClick for config.refs: ['tabs[]'].

on<Child><Event>

ts
import { 
Base
, type
DelegatedEvent
} from '@studiometa/js-toolkit';
class
AccordionItem
extends
Base
<{
$emits
: {
open
: {
height
: number } } }> {
static
config
= {
name
: 'AccordionItem' };
} class
Accordion
extends
Base
{
static
config
= {
name
: 'Accordion',
components
: {
AccordionItem
} };
onAccordionItemOpen
({
event
,
target
,
payload
}:
DelegatedEvent
<
AccordionItem
, 'open'>) {
console
.
log
(
target
.
$id
,
payload
.
height
);
} }
ts
interface DelegatedEvent<T extends Base = Base, K extends string = string> {
  event: Event;
  target: T;
  payload: EmitDetail<PropsOf<T>, K>;
}

target is the child instance. payload is event.detail, typed from the child's $emits.

config.components is what disambiguates the name

onSliderDragStart is SliderDrag + start, or Slider + drag-start. The name set from config.components decides. A child that is not declared there is not resolved.

A lazy child works the same way, because the key is the name and nothing has to be downloaded to resolve a handler.

onWindow<Event> and onDocument<Event>

ts
import { 
Base
, type
GlobalEvent
} from '@studiometa/js-toolkit';
class
ClickOutside
extends
Base
{
static
config
= {
name
: 'ClickOutside' };
onDocumentClick
({
event
}:
GlobalEvent
<MouseEvent>) {
if (!
event
.
composedPath
().
includes
(this.
$el
)) {
this.
$el
.
removeAttribute
('data-option-open');
} } }
ts
interface GlobalEvent<T extends Event = Event> {
  event: T;
  target: Window | Document;
}
  • Phase: bubble, always. onDocumentClick hears what document.addEventListener('click', …) hears. To hear a descendant's non-bubbling event, use on<Ref><Event>.
  • target is the global the handler names. There is no payload and no index.
  • Listener options such as once and passive are not part of this. Use $on() for those.

Delegation

Ref and child handlers are delegated from this.$el:

  • One listener per event type on the root element.
  • The handler walks from event.target up to this.$el, reads the instance map of each element, and calls on<Name><Event> for the first mounted instance that matches.
  • A ref or a child inserted later needs no new binding.
  • Events that do not bubble — focus, blur, scroll, mouseenter, mouseleave — are delegated from the capture phase.

Typing

A method named by convention is not typed by convention: the name is resolved at runtime, so annotate the payload with RefEvent, DelegatedEvent or GlobalEvent.

The @on decorator checks that annotation against a real target instead of leaving it unverified.

Dynamic sets of events

A handler name belongs to the class. When the set of events is data — one subscription per markup declaration, with its own modifiers — bind it yourself and own the cleanup:

js
mounted() {
  return this.$options.events.map((type) => this.$on(type, this.handle));
}

That is the intended path, not a workaround.

MIT Licensed