# Olum (olumjs)
> Olum is a small, Vue-like JavaScript framework where components are plain `.html` files with a
> `
Count: {state.count}
+
```
- The **filename is the component name** — `CounterCard.html` → ` `. Component tags are **PascalCase**; that's how the compiler distinguishes a component from a plain element.
- `
```
---
## Text interpolation
`{expr}` in text is evaluated as JS and **HTML-escaped by default** (XSS-safe).
```html
Hello {state.user.name}
Total: {state.items.length} items
{state.count > 0 ? 'positive' : 'zero'}
```
- `null` / `undefined` render as an empty string.
- No escape for a literal `{` in text — any `{…}` is interpolation. Use `{String.fromCharCode(123)}` or a const holding `"{"`.
---
## Attributes
**String attributes (default):** a literal string; use `{expr}` inside for dynamic parts.
```html
Profile
box
```
**Code attributes (value is an expression):** `when`, `each`, `key`, `on*`, `html` — the `""` value is JS.
```html
…
+
```
**Boolean attributes (presence toggle):** when the whole value is a single `{expr}`, the attribute is emitted when truthy, omitted when falsy.
```html
Save
…
```
Recognized: `checked, disabled, selected, readonly, required, hidden, autofocus, multiple, open, loop, muted, controls, autoplay, novalidate, default, defer, ismap, reversed`. Only applies when value is exactly one `{expr}`.
---
## Events
Native `on*` attributes hold **code** (like real HTML). Two forms:
```html
+
multi call
no-paren arrow
```
- **No event modifiers.** Do it in the handler: `onsubmit="(e)=> { e.preventDefault(); save() }"`.
- **Several `on*` attributes per element are fine** — each is wired as its own listener (` `). Use `onMount` only for listeners on `window`/`document` or ones needing `capture`/`passive`.
- Inside ``, handlers can reference loop variables directly: ``.
- On a **component** tag (any PascalCase tag you import — ` `, ` `, ` `), any `on*` name is a **function prop, NOT a DOM event** — and a function prop takes a **NAME only**. Only the capital letter separates the two cases:
```html
```
- Recognized inline names = the standard DOM `on*` set (`onclick, oninput, onchange, onsubmit, onreset, onformdata, oninvalid, onselect, onsearch, onkeydown, onfocus, onblur, ondragstart…ondrop, onplay, onpause, onloadedmetadata, ontouchmove, …`).
- **NOT recognized inline:** `copy`, `cut`, `paste`, `beforeinput`, `focusin`, `focusout` → `host.querySelector(...).addEventListener(...)` in `onMount`, remove in the cleanup.
**Keyboard:** `keydown` fires first and is the only one you can `preventDefault()` to stop the character; `keyup` last; `keypress` is legacy (misses `Escape`/arrows). Fields: `e.key`, `e.code`, `e.ctrlKey/shiftKey/altKey/metaKey`, `e.repeat`.
```html
```
**Focus/selection:** `focus`/`blur` do **not** bubble — put them on the field itself (`focusin`/`focusout` bubble but aren't inline). Track "touched" on blur to delay errors. Selected text = `field.value.slice(field.selectionStart, field.selectionEnd)` in `onselect`. Focus a field with `host.querySelector("[name=x]").focus()` (no refs).
**Drag & drop:** `ondragover` **must** `preventDefault()` or the browser refuses the drop; dropped files are `e.dataTransfer.files` (a `FileList`). Reorder = `draggable="true"` per row + move the item in the array; add `key` on the `` so rows keep their DOM.
**Clipboard write** is an API, not an event: `navigator.clipboard.writeText(str)` (returns a promise the browser can reject).
---
## Conditionals — `` / `` / ``
`when` is a JS expression. The element is **added/removed** from the DOM.
```html
Tab A
Tab B
Fallback
```
## Show / Hide — ``
Like ``, but keeps content in the DOM and only toggles visibility (`display: contents` ↔ `none`).
Content nodes are never replaced, so a playing ``, animation, or typed input survives toggling.
```html
…
```
It compiles to one stable wrapper `` present in **both** states, so parent CSS with a direct-child
combinator through it (`.parent > .panel`) won't match — target `[data-o-show] > .panel` or drop the `>`.
Don't wrap `
`/`` in `` (its wrapper `` is invalid there) — use `
` instead.
## Loops — ``
`each` is a JS expression. Three forms:
```html
{fruit.name}
Step {i}
{key}
```
Index + array params (like `Array.map`), wrap in parens:
```html
{index+1}/{arr.length}: {cat.name}
{index}. {key} = {value}
```
**Keyed loops — add `key`** (a JS expr, no braces, unique per item) so items are matched by identity across reorders/removals instead of by position. Needed to keep DOM state (checkboxes, videos, typed text) on the right row:
```html
{todo.text}
```
`key` on `` keys both component loops (the instance is reused) and plain-element loops (the repeated root is reused);
it applies to the loop's **direct root only** — inner children are never keyed. `key="{item.id}"` directly on a plain
element does the same thing.
Dynamic count range: `each="i of Array.from({ length: state.n }, (_, k) => k + 1)"` (a literal number is required for the `of N` form).
---
## State & Reactivity
Declare reactive state as a **literal top-level `const state = { ... }`**. Mutating `state` re-renders. Reactivity is **deep** (nested objects, arrays, `Map`, `Set`).
```html
{state.count} — {state.user.name}
```
- Only `state` is reactive. Plain `const`/`let` are not tracked.
- Deep mutations that re-render: `state.user.name = "Bo"`, `state.todos.push(t)`, `state.todos[0].done = true`, `state.tags.add("x")`.
- Non-plain objects (`Date`, DOM nodes, class instances) are NOT tracked — assign back to a top-level key to re-render.
- Re-assigning the **same reference** (`state.todos = state.todos`) is a no-op. Force a re-render with a fresh value (spread, `map`, `filter`, `slice`).
- `const state = makeState()` is NOT recognized — must be a literal object in the component.
### Rendering & updates (there is no render function — writing to state/store IS the API)
1. A write emits (deep mutations included) → 2. dirty components are **batched per microtask** (`state.a++; state.b++`, or a `splice` shifting 100 rows = ONE pass) → 3. the template renders into a **detached** tree → 4. the patcher diffs it against the live DOM and mutates only what changed.
- **Untouched nodes survive**, and with them everything the browser owns: focus, caret/selection, scroll, playing ``/``, running CSS animations, iframes, canvas, uncontrolled input values. Event listeners are kept unless their binding (or a value baked into it, e.g. a loop variable) changed.
- **Only what needs to render, renders.** A write to a state key the **template never reads** doesn't schedule a render at all (so a key used only in handlers/`onMount` is free). Store subscriptions come from *reading* the store during render. If a parent and its child are both dirty in the same tick, only the parent renders (its pass rebuilds the child). Updates for components that left the DOM are discarded.
- **Identity:** component instances have their own; plain elements opt in with `key`. Keyed nodes are **moved** (`moveBefore` where supported), not re-created; unkeyed children are matched positionally with a short insertion/deletion lookahead. A bigger unkeyed reshuffle repaints nodes with a neighbour's data — add `key`.
- **Form controls are never clobbered:** `value`/`checked` are written to a live input only when the **template's** value changed, and the caret is preserved when they are; a `` keeps the user's choice unless the template moves the `selected` option.
- `import { flushUpdates } from "olum"` settles pending re-renders synchronously (idempotent; mostly for tests / imperative DOM reads right after a write). `window.olum.flushUpdates()` is the same call, but `window.olum` exists in DEVELOPMENT only — a production build ships no runtime globals.
- If a patch throws, olum logs `patch failed — falling back to full re-render` and rebuilds that component's content (correct, but browser state resets) — that warning is a bug worth reporting.
### Derived values = plain functions (no "computed")
```html
{doubled()} {quadrupled()}
```
### Watchers — `const watcher = { key(old, next) {} }`
Fires on **top-level key assignment** only (not nested mutations).
```html
```
For a nested change to fire a watcher, assign a fresh value to the key: `state.user = { ...state.user, name: "Bo" }`.
---
## Components & Props
Use a component by its **PascalCase** tag; import it in `
```
**Prop value rules:**
- `title="Hello"` → string (literal).
- `count="{n + 1}"` → the expression's **real value/type** (number, object, etc.) when the whole value is one `{expr}`.
- `greet="Hi {state.name}"` → interpolated **string**.
- **No spread** (` `) and **no shorthand** (` ` / ` `). Pass each field: ` `.
- **Function props travel by NAME** (component tags only — lowercase HTML tags keep normal inline `on*` code). Props are handed over as data, so a function only arrives when the value is a plain name the compiler can resolve: a top-level function of the parent, or a name destructured from `props()`. `onX="(e)=>…"` arrives as a string; `onX="{(e)=>…}"`, `onX="{obj.fn}"` and `onX="{state.fn}"` arrive as `undefined`; `onX="{make(id)}"` passes the call's result. To pre-bind data, pass it as its own prop and let the child call back with it (`
` → child calls `onPick(row.id)`).
- Prop names must be plain camelCase identifiers (dashes truncate the name on component tags).
**Reading props in the child** — `props()` imported from `olum`, called once at the **top level** of `
{label}
```
Destructuring from `props()` is a **compiler feature**: each destructured name compiles into a fresh `props().name` read, so it stays current after the parent re-renders. `{props().label}` is equivalent.
**Stays live:** plain names, aliases, defaults, `children`, function props.
**One-time snapshot (NOT live):** `...rest`, nested patterns `{ user: { name } }`, computed keys `{ [key]: v }`, destructures not at the top level of `
```
Assigning to a destructured prop throws (`const`); never mutate props. On a **component** tag, `on*` names (even `onclick`) are just function props the child chooses when to call — so they follow the NAME-only rule above, and the inline forms allowed on `` do not work on ``. A function prop resolves against the component whose template **authored** the tag, so it still arrives when the tag is written inside another component's slot (``).
---
## Slots — `children`
Content between a component's tags is exposed as `children` on `props()` (injected as **raw trusted markup**, not escaped — don't route untrusted strings through a slot).
```html title="Box.html"
{children}
no content
```
## Scope — public/private surface (advanced)
Top-level props and methods are **private by default**. Scope attributes on the `
```
What counts: **props** = top-level `const`s with a simple initializer (literal, object, array, ternary, another name); **methods** = top-level function declarations or `const fn = () => …`. Anything else (`const x = compute()`) still works but isn't exposed. Reactive `state` is governed by the **props** scope (`exclude-state` keeps it private under a public-props group).
Read a **mounted** component's public surface with `scope("Name"[, index])` from `olum` → `{ props, methods, state, el, key }` (`state` is the live proxy — writing to it re-renders that component; returns `null` + a console warning if nothing matches). Escape hatch — prefer the store for shared state.
---
## Lifecycle — `onMount`
Import `onMount` from `olum`; the callback runs on mount; **return a cleanup function** that runs on unmount (e.g. an `` toggles it off, or a keyed item is removed). Call `onMount` **once** per component.
```html
```
`host` is the component's own root element — use it instead of `document.querySelector`. `onMount` is also where you do **refs**, **actions** (there is no `bind:this` / `use:action`), and **global `window`/`document` listeners** — all wired imperatively and cleaned up in the return.
Same shape for **observers** and anything the element owns: `new ResizeObserver(...).observe(host.querySelector(".box"))` → `return () => observer.disconnect()`; a `` `requestAnimationFrame` loop → `cancelAnimationFrame`; a ``'s `currentTime` read per frame (its `onplay`/`onpause`/`onloadedmetadata` still work inline).
---
## Forms & inputs (two-way binding is manual — there is no `model`)
Two ways to run a form, mixable: **(a)** bind each field to state (value-ish attribute reading state + event handler writing back) when the page reacts as the user types; **(b)** bind nothing, give every control a `name`, read `new FormData(form)` once.
```html
red
```
`value="{expr}"` on the `` element itself silently does nothing — put `selected="{expr}"` on each option.
For ``: `Array.from(e.target.options).filter(o => o.selected).map(o => o.value)`.
**One `set` helper covers the text-like family** (`text, email, password, tel, url, search`, and `date, time, datetime-local, month, week`) — every native attribute (`placeholder, maxlength, minlength, pattern, autocomplete, spellcheck, step, min, max, accept, list`) keeps working as in HTML:
```html
```
- **date/time inputs give a STRING**, never a `Date` (`"2026-08-17"`, `"09:30"`, `"2026-08-17T09:30"`, `"2026-08"`, `"2026-W34"`). Parse yourself: `new Date(state.date + "T00:00:00")`.
- **`type="color"`**: `oninput` fires while dragging in the picker (live preview), `onchange` once when it closes (commit).
- **checkbox group from a list:** `` + `checked="{state.toppings.includes(topping)}"` + `onchange="toggle(topping, $event)"` → concat/filter a new array.
- **`` is only a suggestion list** — the typed value is still free. ` ` + `note `. A `type="range"` can snap to one (``).
- **` `/``** are normal markup (loopable). One `disabled="{expr}"` on a `` switches off every control inside it and drops them from the submitted data.
- **`{total()} `** = live result; **``** = task that finishes (no `value` attribute → indeterminate spinner, toggle it with ``); **``** = a measurement, colors itself.
- **Inputs in a loop:** pass the index — `onchange="toggleDone(index, $event)"` — then write a **new** array back. Typing in one row never disturbs the others (only changed values are written; caret preserved).
**Validation** (native constraints + your own messages):
```html
```
```js
const onInvalid = (e) => { e.preventDefault(); // stop the native bubble, render the message yourself
state.errors = { ...state.errors, [e.target.name]: e.target.validationMessage }; };
const handleSubmit = (e) => { e.preventDefault();
if (!e.target.noValidate && !e.target.checkValidity()) return; /* send */ };
// rule no attribute can express: field.setCustomValidity(ok ? "" : "message") — "" means valid
```
**Form events:** `input`/`change` **bubble**, so one listener on the `