Events
Events in JAF are just HTML.
Put an onclick on a button, point it
at a function from your <script>, and that is the whole
API. Nothing to import, no new syntax.
<script>
let count = 0
function handleClick() {
count++
}
</script>
<button class="tally" onclick="handleClick()">
Clicked {count} times
</button>
<style>
.tally {
padding: 0.55rem 1.1rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
.tally:hover {
border-color: var(--border-hover);
background: var(--bg-hover);
}
</style>This is the form to use almost every time.
Every standard DOM
event attribute behaves the same way: onclick,
oninput, onchange, onsubmit,
onkeydown, and the rest.
Never write onClick. That is React's
spelling. JAF uses the real HTML attribute names, which are all lowercase.
What Goes in the Attribute
The value is an expression evaluated in your component's scope, so it does not have to be a function call:
-
An expression that changes state, with no function at all:
onclick="count++" - A call with arguments:
onclick="pick('Apple')" -
Anything that reads the event:
oninput="typed = event.target.value" -
An arrow in braces, which is the general form and the one to reach for when the handler
needs an argument bound or wants the event:
onclick={() => save(draft)},oninput={(e) => { query = e.target.value }}
<script>
let count = 0
let picked = 'nothing'
let typed = ''
function pick(fruit) {
picked = fruit
}
</script>
<!-- An expression: no function needed -->
<button class="chip" onclick="count++">Add one</button>
<!-- A call with arguments -->
<button class="chip" onclick="pick('Apple')">Apple</button>
<button class="chip" onclick="pick('Banana')">Banana</button>
<!-- event is in scope inside the attribute -->
<input placeholder="Type something" oninput="typed = event.target.value">
<p>Count {count}, picked {picked}, typed {typed}</p>
<style>
.chip {
margin-right: 0.4rem;
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
cursor: pointer;
}
.chip:hover {
border-color: var(--border-hover);
background: var(--bg-hover);
}
</style>Count 0, picked nothing, typed
A bare name is called for you.
onclick="handleClick" runs handleClick(). That
applies to any bare name, so onclick="count" tries to
call count(). When you mean an expression, write one.
event only exists inside the attribute. A
function declared in <script> cannot reach it. If a
function needs the event, hand it over:
onclick="record(event)".
When You Need a Modifier: on:
Some handlers open with the same boilerplate every time -
event.preventDefault(), event.stopPropagation(), a
check that the key pressed was Enter.
The on: directive moves that into the attribute name, so the
handler is only your own code.
<script>
let log = 'Nothing yet'
function handleSubmit() { log = 'Submitted, and the page did not reload' }
function handleLink() { log = 'The link did not navigate' }
function handleOnce() { log = 'This handler has now detached itself' }
</script>
<!-- preventDefault: the browser does not reload the page -->
<form class="row" on:submit|preventDefault={handleSubmit}>
<input placeholder="Anything">
<button class="chip" type="submit">Submit</button>
</form>
<!-- Chain modifiers with | -->
<p><a href="/nowhere" on:click|preventDefault|stopPropagation={handleLink}>A link that goes nowhere</a></p>
<!-- once: the listener removes itself after the first event -->
<button class="chip" on:click|once={handleOnce}>Click me repeatedly</button>
<p>{log}</p>
<style>
.row {
display: flex;
gap: 0.5rem;
align-items: center;
}
.chip {
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
cursor: pointer;
}
.chip:hover {
border-color: var(--border-hover);
background: var(--bg-hover);
}
</style>Nothing yet
Three rules cover the whole directive:
-
on:click={handleClick}means "callhandleClickwhen this element is clicked". A bare name is called for you, exactly as in anonclick. -
Modifiers sit between the event name and the
=, each one after a|:on:submit|preventDefault={save}. Chain as many as you like. -
Prefer
onclick="..."unless you need a modifier. Both compile to one real listener over your component's scope; the directive only adds the modifiers.
Inside an on: value, event is in scope and
this is the element the listener is attached to.
Modifier Reference
| Modifier | Effect |
|---|---|
preventDefault |
Calls event.preventDefault() before your handler |
stopPropagation |
Calls event.stopPropagation() before your handler |
once |
The listener detaches itself after the first event |
self |
Runs only when the event started on this element, not on a descendant - how a backdrop closes on an outside click |
capture |
Listens in the capture phase |
passive |
Marks the listener passive, for smoother scrolling |
That is the complete list.
stopImmediatePropagation and trusted are not
implemented.
Unknown modifiers are dropped silently. A typo does not
error, it removes the guard:
on:click|prevenDefault={submit} attaches a listener with no
preventDefault at all. The separator is a pipe, so
on:keydown.enter is not recognised either.
Keyboard Modifiers
Filter by key without touching event.key:
<script>
let log = 'Nothing yet'
function onEscape() { log = 'Escape pressed' }
function onEnter() { log = 'Enter pressed' }
function onCtrlEnter() { log = 'Ctrl + Enter pressed' }
</script>
<input placeholder="Press Escape" on:keydown|escape={onEscape}>
<input placeholder="Press Enter" on:keydown|enter={onEnter}>
<!-- Meta keys combine with a key -->
<input placeholder="Press Ctrl + Enter" on:keydown|ctrl|enter={onCtrlEnter}>
<p>{log}</p>
Nothing yet
| Modifier | Key |
|---|---|
escape, enter, tab,
space |
Escape, Enter, Tab, Space |
up, down, left, right
|
Arrow keys |
delete, backspace |
Delete, Backspace |
ctrl, alt, shift, meta
|
Meta keys, combinable with any of the above |
There are no letter keys and no esc alias.
on:keydown|ctrl|s={save} drops the s and fires on
Ctrl plus any key; on:keydown|esc={close} drops the
guard and fires on every keydown. For a letter shortcut, check
event.key in the handler yourself.
Where a Handler Belongs
A handler is compiled wherever it is written: the attribute is stripped, the body becomes a
closure over your component's scope in its handler table, and the runtime calls
addEventListener. That holds in the static template, in a
<for> row, in a <show>,
<switch> or <guard> arm, in a
<teleport> body and in an overlay body - curried over the loop variables
where there are any.
capture, passive and once are real
addEventListener options in every one of those positions. In a row
once is per row, not per list.
Changed in 0.1. Modifiers used to be lost inside a
<for> row three ways at once, and .map() was the
documented workaround. It is no longer needed for this. An on: value may
also contain braces now - on:click={() => { count++ }} and
on:click={() => save({ id })} are read with a nesting-aware scan
instead of truncating at the first }.
A Handler on a Component Tag
<Button onclick="save()"> and
<Button on:click|preventDefault={save}> both work, and the handler is
your code: it reads and writes the variables of the file it is written in, not the
child's. The compiler puts the body in your handler table and the child attaches it to its
own root element, so no callback is passed as a prop and nothing is rewritten at runtime.
Modifiers behave exactly as they do on an element of your own.
<!-- The handler is the parent's; the listener is on the child's root -->
<Button onclick="save()">Save</Button>
<Button on:click|preventDefault={save}>Save</Button>
Changed in 0.1. This used to be a build error, and before that the
alpha forwarded the attribute text onto the child's root element and rewrote it at
mount. Older notes telling you to wrap the tag in a <span>, or to
pass a callback as a prop, can go: props are strings, and this never needed to be one.
Four shapes are still build errors, each named:
-
the child declares a variable of that name (
let onclick), so the tag is passing it a prop and not writing a handler; -
the tag writes
noprops, or the child's script isno-forward- nothing forwards, so the handler would have nothing to attach to; - the child's template has no single root element (two roots, or an expression at the root), so which element the listener lands on is not knowable at build time;
- the compiler could not read the handler at all.
In all four, put the handler on an element of your own around the tag or - better - inside
the component: a <Button> that owns a click has somewhere to put it, and
a prop it declares can say what the click means.
One position is still different, and it says so rather than going quiet:
-
The
on:directive is dropped in another component's slot content, with a[jaf]warning naming the event and the tag. That markup is mounted by the component you passed it to, so the handler's names would resolve against its scope. A plainonclick="go()"in the same position does work: JAF compiles an inline handler back into the template that wrote it.
One spelling is positional rather than refused: the brace-arrow attribute,
onclick={() => select(item)}, belongs on compiled markup - an element of
your own, or a <for> row, where it closes over the row's variables. In a
{items.map(...)} row it is a build error whether or not the row is keyed. An
unkeyed .map() row refuses every handler spelling; a keyed one takes
the interpolated string form (onclick="select({item.id})"), because those rows
are compiled into a list module of their own. See
for for which shapes belong in which list.