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>
output

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>
output

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>
output

A link that goes nowhere

Nothing yet

Three rules cover the whole directive:

  • on:click={handleClick} means "call handleClick when this element is clicked". A bare name is called for you, exactly as in an onclick.
  • 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:

source
<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>
output

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.

source
<!-- 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 is no-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 plain onclick="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.

  • Props - what forwards to a component's root element, and what is refused
  • bind: - two-way binding, instead of writing oninput by hand
  • Security - how values interpolated into a handler string are encoded