# 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. ```html
onSubmit(form, () => form = empty())}>...
``` Event handler parameters are typed per element and per event. Writing ` ``` ```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 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. ```html ``` On initial render, truthy translates to `autofocus`. Subsequent flips call `focus()` or `blur()`. **iOS focus footgun.** 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 import { flush } from "@elements/app"; function openSearch() { open = true; flush(); inputEl.focus(); } ```