Events

An event is an on* attribute holding a function. The handler receives the native DOM event. That is the entire model:

html`<button onclick="${() => save()}">Save</button>
     <input onkeydown="${(e) => handle(e)}" />`

Component callbacks are the same thing one level up: lowercase function props:

html`<${Card} onsave="${(v) => api.save(v)}" />`

Event naming is inconsistent across environments, and the failure mode is expensive: a handler that attaches under the wrong name fires silently never. LemonadeJS enforces one rule, that all event and callback names are lowercase, to make that failure mechanically impossible.

Mechanics

One casing rule, no exceptions. Every event and callback name is lowercase, HTML-style: onclick, oninput, onchange, onsave, onitemclick, exactly how the platform spells onmousedown. Any other casing warns LJS-305 in dev, on elements and on component props. On native elements the event name is normalized so a stray onMouseDown still attaches (and warns); on components, where a wrong-cased prop would be silently ignored, the tripwire is the whole point.

Functions only, enforced. An on* attribute that does not hold a function fails hard with LJS-301 at mount, in dev and production alike. String handlers do not exist, so templates are CSP-safe by construction, and since the engine contains no eval or new Function anywhere, the zero-build deployment runs under a strict CSP.

Native events, no synthetic layer. The handler receives the browser’s event object. onchange is the DOM change event; oninput is input. What MDN documents is what happens.

Attach once, resolve per dispatch. The engine adds one listener per on* attribute and reads the current handler through the template’s value holder at dispatch time, so when a branch re-renders with a new closure, the handler is swapped without any removeEventListener/addEventListener churn.

Component callbacks

A component event is nothing more than a lowercase function prop: declared in the contract as an event:

export const Datagrid = component('datagrid', {
    // ...
    onchange: Function,           // (row, columnName, value, oldValue)
    onselect: Function,           // (selectedRows)
    onsort: Function,             // (columnName, direction | null)
    oncolumnresize: Function,     // (columnName, widthPx) on handle release
}, (props) => { /* ... */ });

The declaration buys every deployment at once: inside LemonadeJS the callback is called directly; in the custom-element deployment each declared event dispatches a real CustomEvent (bubbling, composed) named after the prop minus on, so addEventListener('change', ...), Vue’s @change, Angular’s (change) all just work, and the React adapter maps it back to a React-style callback. Passing a non-function for a declared event warns LJS-401.

For the two-way onchange protocol (value.set() commits and fires onchange; parent writes never echo back), see Two-way binding. And anything that closes or commits on focusout should check isDisposing(), because the renderer removing a focused element fires focusout exactly like a user leaving; see Destroy.

In practice

The Tabs block header (components/tabs, shipped source): one <ul>, seven native handlers, all plain functions: click, focus-follows-arrows, keyboard, and drag-sorting:

<ul ref="${(el) => (ul = el)}"
    onclick="${onOpenEvent}"
    onfocusin="${onOpenEvent}"
    onkeydown="${onKeydown}"
    ondragstart="${onDragstart}"
    ondragover="${(e) => e.preventDefault()}"
    ondrop="${onDrop}"
    ondragend="${onDragend}">

And the Switch block shows both layers in one line each: the native change event of its real <input type="checkbox"> feeds the bound state, and the component’s onchange fires only for user-initiated commits, because the commit goes through set():

<input type="checkbox" class="lm-switch-input"
    onchange="${(e) => current.set(e.target.checked)}" />

A parent writing enabled.value = true programmatically updates the switch without echoing onchange back, the protocol that prevents update loops between parent and child.

Reference

onclick="${fn}"            // any on* attribute; fn receives the native event
on*function, always    // LJS-301 (throws): strings never
onclick, onchange, onsave  // lowercase, always; LJS-305 warns otherwise
<${C} onsave="${fn}" />    // component callback: a lowercase function prop
onsave: Function           // contract declaration → CustomEvent 'save' on <lm-c>
  • Handlers are attached once per attribute; branch updates swap the function without re-attaching the listener.
  • Declared contract events must be lowercase (they become HTML-facing names); non-function values warn LJS-401.
  • isDisposing() distinguishes renderer-caused focusout from the user’s.