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-causedfocusoutfrom the user’s.