Components & Props
Use a component by its PascalCase tag. Import it in <script>.
<script> import StatusBadge from "./StatusBadge"; import CounterCard from "./CounterCard"; const state = { score: 5, name: "Ann" }; </script> <StatusBadge label="Online" color="#16a34a" /> <CounterCard title="Counter" initialValue="{state.score}" />
Props rule
A prop is a literal string by default; a whole-value prop="{expr}" passes the expression's real value/type; {} inside text yields an interpolated string.
<Comp title="Hello" <!-- "Hello" → string --> count="{n + 1}" <!-- 6 → number (type kept) --> data="{state.user}" <!-- {...} → object (type kept) --> greet="Hi {state.name}" <!-- "Hi Ann" → interpolated string --> />
Reading props in the child
props() is imported from olum and returns this instance's current props. Call it directly at the top level of <script> — no onMount() required.
<!-- StatusBadge.html --> <script> import { props } from "olum"; const { label, color } = props(); // destructured names stay LIVE — see below </script> <span class="badge" style="color:{color}">{label}</span>
Destructuring from props() is a compiler feature in OlumJS. The compiler rewrites destructured prop variables into live property accesses, so they always reflect the latest prop values.
Concretely: every use of label/color — in templates, methods, and event handlers alike — compiles into a fresh props().label / props().color read, so the names stay current after the parent re-renders. Writing props().color directly is equivalent; use whichever reads better.
What stays live
const { color } = props(); // ✓ live everywhere: template, methods, handlers const { color: c } = props(); // ✓ alias — c reads the parent's `color` const { size = "md" } = props(); // ✓ default when the parent omits it const { theme: t = "dark" } = props(); // ✓ alias + default combined const { children } = props(); // ✓ slot content — live too const { onChange } = props(); // ✓ function props — live callback
Liveness follows normal scoping: a loop variable (<for each="color of list">) or a function parameter with the same name shadows the prop inside that scope — the local wins, exactly like plain JavaScript.
What keeps a one-time snapshot
A few patterns can't be made live — they compile untouched and keep the value from creation time (never an error, just not fresh):
const { a, ...rest } = props(); // ✗ rest is a snapshot (a is still live) const { user: { name } } = props(); // ✗ nested pattern — snapshot const { [key]: v } = props(); // ✗ computed key — snapshot onMount(() => { const { a } = props(); // ✗ not top level of <script> — scoped snapshot }); const p = props(); const { a } = p; // ✗ indirect — destructure props() directly const { a, b = a } = props(); // ✗ a default referencing another destructured // prop reads its stale value — keep defaults // self-contained
And since props are read-only, don't assign to a destructured name — color = "x" throws Assignment to constant variable; change parent-owned values through a callback prop instead.
Default values
Plain destructuring defaults give a prop a fallback for when the parent omits it:
<!-- Nested.html --> <script> import { props } from "olum"; const { answer = "a mystery" } = props(); </script> <p>The answer is {answer}</p>
<Nested answer="{42}" /> <!-- The answer is 42 --> <Nested /> <!-- The answer is a mystery -->
There is no spread shorthand (<Comp {...obj} />) — pass each field explicitly: <Info name="{pkg.name}" version="{pkg.version}" />.
Function props — component events
A prop can be a function. That's how a child talks up to its parent: the parent passes a handler down, the child calls it with any payload.
<!-- parent --> <script> import Inner from "./Inner"; const handleMessage = (payload) => alert(payload.text); </script> <Inner onMessage="{handleMessage}" />
<!-- Inner.html --> <script> import { props } from "olum"; const { onMessage } = props(); const sayHello = () => onMessage({ text: "Hello!" }); </script> <button onclick="sayHello()">Click to say hello</button>
The value must be a name, never an inline function
Props cross into the child as data, so a function can only travel as a reference the compiler can look up by name. Two names resolve: a top-level function of this component, and a name destructured from props() (forwarding, below).
<script> import Input from "./Input"; // YOUR component, in Input.html const handleInput = (e) => (state.text = e.target.value); </script> <Input oninput="{handleInput}" /> <!-- ✓ a top-level function of this component -->
Input here is only an example name for a component you wrote — read it as any component, <TextField/>, <CounterCard/>, <Comp/>. The capital letter is the whole difference: a PascalCase tag is a component and gets props, a lowercase tag (<input/>, <button/>) is the real HTML element and gets DOM events. The two examples below say the same thing with the pair that is easiest to mix up.
Anything else is not a function by the time the child reads it — the compiler does not report it, the child throws oninput is not a function when it calls the prop:
<!-- capital I — <Input/> is a COMPONENT of yours, so oninput is a prop, not a DOM event --> <Input oninput="() => console.log(123)" /> <!-- ✗ arrives as the STRING "() => console.log(123)" --> <Input oninput="{() => console.log(123)}" /> <!-- ✗ inline function — dropped, arrives undefined --> <Input oninput="{state.handlers.input}" /> <!-- ✗ not a plain name — dropped, arrives undefined --> <Input oninput="{makeHandler(id)}" /> <!-- ✗ passes the call's RESULT, not a function -->
The fix is always the same: give the function a name in <script>, then pass the name.
This rule is for component tags (PascalCase) only. Real HTML elements (lowercase) are unchanged — <input oninput="state.counter++" /> and <input oninput="e => console.log(123)" /> still work exactly as Events describes, because there on* is real DOM code, not a prop. Same six letters, opposite rules:
<input oninput="e => console.log(123)" /> <!-- ✓ real <input> element — inline code is fine --> <Input oninput="e => console.log(123)" /> <!-- ✗ YOUR <Input/> component — needs a name: "{handler}" -->
Need a handler that knows about a loop item? You can't pre-bind it in the tag — pass the data down and let the child hand it back:
<!-- ✗ an inline arrow can't bind the row here --> <for each="row of state.rows" key="row.id"> <Row onPick="{() => pick(row.id)}" /> </for> <!-- ✓ pass the row, let the child call back with it --> <for each="row of state.rows" key="row.id"> <Row row="{row}" onPick="{pick}" /> <!-- child: onPick(row.id) --> </for>
Forwarding through a middle component is just passing the prop along:
<!-- Outer.html — sits between parent and Inner --> <script> import Inner from "./Inner"; import { props } from "olum"; const { onMessage } = props(); </script> <Inner onMessage="{onMessage}" />
A function prop also survives being written inside another component's slot — the handler is resolved against the component whose template contains the tag, not the one that happens to render it:
<script> import Section from "./Section"; import Card from "./Card"; const pick = (id) => (state.picked = id); </script> <!-- Card is authored here but instantiated during Section's render — `pick` still arrives --> <Section> <Card onPick="{pick}" /> </Section>
On a component tag, an on* name is just a prop like any other — even onclick. <CustomButton onclick="{handleClick}" /> hands the child a function; the child decides when to call it (e.g. from its own <button onclick="onclick()">). Only on plain elements is on* a real DOM event — which is why <CustomButton onclick="() => …" /> does not work while <button onclick="() => …"> does.
Props are read-only (one-way data flow)
Data flows down through props; changes flow up through callbacks. Assigning to props().x does nothing except log a console warning — to change a parent-owned value, the parent passes a callback prop and the child calls it:
<!-- parent: owns the value, hands the child a way to request changes --> <script> const state = { score: 0 }; const setScore = (n) => (state.score = n); </script> <CounterCard value="{state.score}" onChange="{setScore}" /> <!-- CounterCard.html: reads the prop, calls the callback to update it --> <script> import { props } from "olum"; const { value, onChange } = props(); // both live — value is always the latest const inc = () => onChange(value + 1); </script>
The owner's state assignment triggers the re-render, and the fresh value flows back down as a prop. For a value that several components read and write, skip the prop threading entirely and put it in the global store.
See the working example at /forms/callback-props (a keypad that edits its parent's passcode via onChange).
Prop names are identifiers
A prop name must be a plain identifier (letters, digits, underscore) — write them in camelCase. A dashed name like data-x="1" doesn't parse as you'd expect on a component tag (the dash truncates the name), so reserve dashed attributes for plain elements.