How do I handle DOM events?
data-event
<button data-event="click:_handleDelete:@dataset:id,@target">Delete</button>
<input data-event="input:_handleName:@value">
data-event="eventName:handlerMethodName[:arg1,arg2,...]"
eventName is bound via addEventListener to this[handlerMethodName](...resolvedArgs). Listeners are rebound (old ones torn down first) on every render, and fully removed on destroy(). If eventName or handlerMethodName is empty, the attribute is ignored.
A handler name the component doesn't have is caught twice:
- At build time (TypeScript): the build checks every handler named in a component's or view's template against its class, so
data-event="click:_svae"failstscat the template's line (Property '_svae' does not exist … Did you mean '_save'?). The same check makes template-only handlers count as used, sonoUnusedLocalsdoesn't flag them. A handler inherited from a parent class must beprotected, notprivate. Values built with${}aren't checked. - At runtime: the attribute binds nothing and a
console.warnnames the component and the missing method (dropped from production builds, like everyconsolecall). This is the only check in JavaScript projects.
Resolving arguments
Each comma-separated arg after the handler name is resolved per-token. A comma inside a quoted literal ('a,b') doesn't split it:
| Token | Resolves to |
|---|---|
@id / @class / @name / @tag |
the matching element property |
@value |
the input/textarea .value, or a contenteditable element's .innerText |
@checked / @selected |
boolean |
@textContent / @innerText / @html |
the matching element property |
@event |
the raw Event object |
@target |
event.target |
@x / @y |
event.clientX / event.clientY (defaulting to 0) |
@key / @code |
event.key / event.code |
@attr:name |
element.getAttribute('name') |
@dataset:key |
element.dataset['key'] |
"literal" / 'literal' |
the literal string, unquoted |
42 |
the literal number |
Anything else falls back to the raw token text unless a custom token has been registered for it (see below).
<button data-event="click:_addToCart:42,"gift"">Add</button>
Custom tokens
TokenHelper is exported from core/index.ts:
import { TokenHelper } from '../../../core/index.js';
TokenHelper.registerCustomToken('@timestamp', () => Date.now());
TokenHelper.registerPrefixedToken('@style:', (prop, el) => (el.style as any)[prop] ?? '');
registerCustomToken(name, resolver)— for a fixed token with no variable part, e.g.@timestamp.registerPrefixedToken(prefix, resolver)— for a repeatable pattern with a dynamic suffix, e.g.@style:color,@style:width.
A prefixed token used alone as a data-event arg (e.g. data-event="click:_handler:@style:color") resolves correctly — the parser rejoins everything after the handler name on : before splitting on ,. Only combining a prefixed token with a comma-separated sibling arg introduces ambiguity worth avoiding; if unsure, test the resolved value directly with TokenHelper.resolve(token, el, event).
Important
- A
data-eventhandler only cancels the defaults that would leave the page: a click on an<a href>, asubmit, and a click on a submit button inside a form — with or without args. Every other default (typing, checking a box, …) is kept; pass@eventand callpreventDefault()yourself to cancel one. - A ctrl/meta/shift/alt or non-primary click on an
<a href>skips the handler, so the browser can open the link in a new tab or window. - A component only binds its own
data-eventelements: those inside a nested child component are bound by that child, to its own methods. - Token values are raw, not HTML-escaped (
a & bstaysa & b). They're escaped when rendered through anhtmltemplate.@checked,@selected,@event,@target,@x,@yreturn booleans/objects/numbers, not strings.