Skip to content

@on

ts
on(type: string)
on(target: typeof window | typeof document, type: string)
on(child: BaseConstructor | string, type: string)

Binds a method to an event source, in place of the on<X><Event> naming convention.

Every form

ts
@
component
({
name
: 'Demo',
components
: {
AccordionItem
},
refs
: ['dots[]'] })
class
Demo
extends
Base
{
// The component's own element, typed from HTMLElementEventMap. @
on
('click')
a
(
event
: MouseEvent) {}
@
on
('submit')
b
(
event
: SubmitEvent) {}
// A ref, named as it is declared. @
on
('dots[]', 'click')
c
({
index
}:
RefEvent
) {}
// A child, by name — imports nothing, and the payload stays `unknown`. @
on
('AccordionItem', 'open')
d
({
payload
}:
DelegatedEvent
<
AccordionItem
>) {}
// A child, by class — the class is the type, so the payload is typed. @
on
(
AccordionItem
, 'open')
e
({
payload
}:
DelegatedEvent
<
AccordionItem
, 'open'>) {}
// A global. @
on
(
window
, 'resize')
f
({
event
}:
GlobalEvent
<UIEvent>) {}
@
on
(
document
, 'click')
g
({
event
}:
GlobalEvent
<MouseEvent>) {}
}

The one-argument form

It types its event from HTMLElementEventMap, so @on('click') hands over a MouseEvent and @on('submit') a SubmitEvent.

A name outside that map is a component event, whose detail only its emitter knows, so the handler declares the type it expects:

ts
@
component
({
name
: 'Slot' })
class
Slot
extends
Base
{
@
on
('content')
inject
(
event
:
CustomEvent
<{
content
: string }>) {
this.
$el
.
innerHTML
=
event
.
detail
.
content
;
} }

A name is a child or a ref

Resolved children-first, so the handler is typed as DelegatedEvent or RefEvent.

A ref is named as it is declared: @on('dots[]', 'click') for config.refs: ['dots[]']. The rule is one rule — the declaration spelling refers to the entry, and the property spelling is used where a name is derived from it.

A mismatched @on('dots', 'click') gives a warning at bind time when the other spelling is declared. A name that matches nothing stays silent.

A class resolves to its merged config.name

It lands on the same delegated entry as the string form, so the two behave identically at runtime. They differ in what they can type:

Formtargetpayload
@on(AccordionItem, 'open')AccordionItemtyped from its $emits
@on('AccordionItem', 'open')the annotationunknown

Only the class form carries the payload type, because only it has the class to read $emits from. The string form's event name is a plain string as far as the type system is concerned, so annotate it as DelegatedEvent<AccordionItem> and narrow the payload yourself:

ts
@
component
({
name
: 'Accordion',
components
: {
AccordionItem
} })
class
Accordion
extends
Base
{
@
on
('AccordionItem', 'open')
byName
({
payload
}:
DelegatedEvent
<
AccordionItem
>) {
(
payload
as {
height
: number }).
height
;
} @
on
(
AccordionItem
, 'open')
byClass
({
payload
}:
DelegatedEvent
<
AccordionItem
, 'open'>) {
payload
.
height
; // number, with no cast
} }

A lazy child needs the string form

@on('Child', 'open') imports nothing. A thunk is not a target, and both the overloads and the runtime refuse it — so a lazy child is exactly the case that trades the typed payload for the deferred import.

Globals

@on(window, 'click') types the event from WindowEventMap and falls back to Event. It goes through the same binding onWindow<Event> uses: bubble phase, one listener per mount cycle, removed by $unmount().

Nothing is reserved in its string space. @on('Window', 'resize') means the child named Window; @on(window, 'resize') means the global. That is the escape from the reserved onWindow prefix.

Any other EventTarget is refused

By the overloads, and by a TypeError at runtime. A decorator is evaluated once, at class definition, so an arbitrary target could only ever be a module-scope value — and binding a component's lifetime to one would be a leak with no way to see it.

It checks the annotation; it does not replace it

A decorator cannot infer a method's own parameters: the method signature is checked against what the decorator expects. So annotate the payload either way — with @on, the annotation is verified against a real target instead of being taken on trust.

Stacking

The skip is keyed by the method name, so @on stacks with itself and with @read/@write in either order:

ts
@
component
({
name
: 'Demo',
components
: {
Child
} })
class
Demo
extends
Base
{
// One method, two events. @
on
('Child', 'open')
@
on
('Child', 'close')
track
() {}
// A phase decorator nearest the method schedules the handler's body. @
on
('click')
@
write
paint
() {}
}

See @read / @write.

The function form

The on<X><Event> method names. Identical behaviour, no build step. See Events hooks.

MIT Licensed