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 name | Listens to | Payload |
|---|---|---|
on<Event> | the component's own element | the 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>
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>
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);
}
}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>
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);
}
}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>
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');
}
}
}interface GlobalEvent<T extends Event = Event> {
event: T;
target: Window | Document;
}- Phase: bubble, always.
onDocumentClickhears whatdocument.addEventListener('click', …)hears. To hear a descendant's non-bubbling event, useon<Ref><Event>. targetis the global the handler names. There is nopayloadand noindex.- Listener options such as
onceandpassiveare 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.targetup tothis.$el, reads the instance map of each element, and callson<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:
mounted() {
return this.$options.events.map((type) => this.$on(type, this.handle));
}That is the intended path, not a workaround.