# Events Event handlers and their naming conventions, template lifecycle, animating removal, focus management, and flushing pending DOM updates. Event handlers are `on`-prefixed attributes. Any TS expression that resolves to a function works. ```ehtml Event handlers
onSubmit(form, () => form = empty())}>...
``` ### Event handlers run in the browser An event handler, and every function it calls other than an `@rpc` function, runs in the browser. It cannot call `sql()`, `tx()`, `session.login()` or any other server-only function. The compiler does not allow it: the build reports a security error at the call site. To reach the database from a handler, call an `@rpc` function. The RPC function runs on the server, and its code, including its queries, is never sent to the browser. ```ehtml Calling the database from a handler /** @rpc */ export function deleteMessage(id: string) { session.isLoggedInOrThrow(); sql(` delete from messages where id = ${id} and authorId = ${session.getOrThrow("userId")} `); } ``` Writing `onclick={() => sql(...)}` instead is a compile error, not a shortcut. ### Typed parameters Event handler parameters are typed per element and per event. Writing ` ``` ```css style.css .message.is-leaving { animation: leave 200ms ease forwards; } @keyframes leave { to { opacity: 0; transform: translateX(1rem); } } ``` `m.leaving` is transient client-side UI state (declare it as `leaving?: boolean` on the row type). Setting it re-runs the `class` binding, so `is-leaving` lands and the CSS animation plays. `onanimationend` then performs the real removal: `messages.delete(m)` for a LiveTable, or a splice for a plain array. The row stays mounted the whole time because it is still in the collection. The `if (m.leaving)` guard in `onAnimationEnd` keeps an enter animation on the same element from triggering the delete. **Use element-attribute handlers (`onclick={...}`) over `addEventListener`** for events Elements supports as attributes. The attribute path goes through the runtime's event wiring; `addEventListener` bypasses it and can surface as inert behavior under iOS quirks like passive-listener and capture-vs-bubble. Reach for `addEventListener` only for events not supported as attributes (`popstate`, `hashchange`) or page-wide listeners that must live above any single template. **Don't put `ontouch*` on `` or other broad ancestors.** When iOS Safari sees a non-passive touch listener on an ancestor, it delays or eats clicks on child elements while waiting to see if the listener will preventDefault. Bind on the smallest reasonable target. **Clipboard UI must not gate on the write succeeding.** `navigator.clipboard.writeText` throws synchronously on insecure origin (iOS Safari over LAN). A `.catch()` won't catch it. Set the "copied!" UI state first, then attempt the write inside try/catch. ## Focus `focus={expr}` is a reactive focus binding. ```ehtml Focus binding ``` On initial render, truthy translates to `autofocus`. Subsequent flips call `focus()` or `blur()`. **Focusing an input on iOS.** iOS Safari only opens the soft keyboard when `focus()` runs on an input that was already in the DOM at the start of the user gesture. Inputs mounted via `e:if` mid-gesture are out-of-gesture and silently denied. Two fixes: 1. Always-mount the input. Hide it with `visibility: hidden`. Don't use `display: none`, which also disqualifies focus. 2. Use `flush()` to mount the input synchronously inside the gesture. **Focusable elements only.** `onfocus`/`onblur` won't fire on a `
` or `
  • ` unless `tabindex={0}` is set. ## Flush `flush()` synchronously drains pending reactive updates. Use only when a browser API requires the DOM to update mid-handler. The most common case is iOS soft-keyboard focus inside a click handler. ```ts Flush import { flush } from "@elements/app"; function openSearch() { open = true; flush(); inputEl.focus(); } ```