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