Components
A component is a .brm file. It has scripts for its state and logic, an optional style, and markup. Other components import it and render it with props.
The parts of a file
<script module>
// Runs once, when the module loads. Its exports can be imported by other files.
export const MAX = 10;
</script>
<script>
// Runs once for each instance, on the server. Declares the state of the component.
import { $signal } from 'brumate/signals';
let count = $signal(0);
</script>
<style>
/* Applies only to this component. */
button {
font-weight: bold;
}
</style>
<!-- Everything else is markup. -->
<button onClick=(() => count++)>{count} of {MAX}</button>
<script>is the instance script. It runs on the server when the component renders, and never runs again in the browser. Code in it that the browser needs, such as an event handler, is split out and loaded when it is needed.<script module>runs once per module. Use it for constants, helpers and data that every instance shares.<style>is scoped to the component. Styles describes it.- The markup can come before or after the scripts. Each part is optional.
The compile-time functions, such as $signal and $props, come from brumate/signals. The compiler replaces their calls, so nothing of brumate/signals ships to the browser.
Using a component
Import the file, and render it with a capitalized tag. Attributes are its props:
The sample runs when it comes into view.
Props
$props() returns the props of the component. Destructure the call, so each prop stays a name that the compiler can follow.
const { title, size = 'medium', ...rest } = $props<{
title: string;
size?: 'small' | 'medium' | 'large';
id?: string;
}>();
- The type argument types the props. Callers get type errors for missing or wrong props, in the editor and in
brumate-tsc. - A default value, such as
size = 'medium', applies when the caller does not pass the prop. - A rest element, such as
...rest, collects the other props. Spread it onto an element with{...rest}. - On an element with a spread, the attributes apply in their order, so a later one wins. A
classadds to the others:<a class="link" {...rest}>withclass="primary"inrestgetsclass="link primary". - Read props by their names. Do not keep the object of
$props()and read from it later.
Props that change
A prop changes in the browser only when the parent passes a value that can change there, such as a signal. The compiler finds these props across the project, so a prop that every parent passes as a constant ships no update code.
The sample runs when it comes into view.
Props that the component writes
Destructure $props() with let to write a prop. $bind in the parent passes its own value, so the child's writes change it.
The sample runs when it comes into view.
- Without a binding, the component writes its own copy. A new value from the parent replaces the copy.
Bindable<T>in the type marks a prop that a parent can bind.- A component can bind a
letprop to its own child, so a write goes up more than one level. - A
constprop stays read-only. Writing it is a compile error. $bindalso works onvalueandcheckedof form fields:<input value=$bind(draft)>.
Controlled and uncontrolled
A part such as a dialog keeps its own state, or writes the state of its caller. A let prop does both, so the caller picks:
<script>
import { $props } from 'brumate/signals';
import type { Bindable } from 'brumate/signals';
const { onChange } = $props<{ onChange?: (open: boolean) => void }>();
let { open = false } = $props<{ open?: Bindable<boolean> }>();
function toggle() {
open = !open;
onChange?.(open);
}
</script>
<button aria-expanded=open onClick=toggle>Details</button>
- Without
open, or withopen=(true), the part keeps its own copy. A new value from the caller replaces it. - With
open=$bind(settingsOpen), the part writes the caller's signal. - With
open=$bind(settingsOpen, (next) => …), the setter decides. The part shows what the caller keeps.
A part that needs to know how the caller passed a prop asks with $isPassed and $isBound from brumate/signals:
let { value } = $props<{ value?: Bindable<string | null> }>();
// True for value=(null) too, so null can be a value that the caller passed.
const passed = $isPassed(value);
// True for value=$bind(selected).
const bound = () => $isBound(value);
Forwarding a ref
A component that renders an element for its caller can pass the element on. It declares ref as a bindable prop, and puts ref=$bind(ref) on the element:
The sample runs when it comes into view.
refis an ordinary bindable prop of the component. The caller binds it with$bind, as it binds any other.- The component writes the element through the binding, so the signal of the caller gets it. Without a binding, the element stays in the component.
- A component can pass the binding on:
<Inner ref=$bind(ref) />. - A dynamic tag that names a component passes
refas a prop too:<{as} ref=$bind(root)>. - The server does not send the element. Handlers and
$clientcallbacks of the caller read the element in the browser, as they read the element of arefin their own file. - A
$client(…, 'visible')of the caller does not wait for a forwarded element. It runs when the element that follows it comes into view.
Functions as props
A component cannot write the signals of its parent. To let a child change a value, the parent passes a function that writes it:
The sample runs when it comes into view.
- The function runs in the browser, and writes the parent's signal. The parent then passes the new value down.
- A function passed as a prop must be code that can run in the browser, such as an arrow function or a function of the parent's script.
- The call runs at once and returns the result, as on the server, so the child can call a function prop in markup or a memo, such as a
filter. The same holds for a function in a context.
Handlers in a spread
A handler that the caller passes, such as onClick, goes through {...rest} to the element, with its wrappers of brumate/events. The element keeps its own handlers too, and both run, its own first:
<!-- Button.brm -->
<script>
import { $props, $signal } from 'brumate/signals';
const { label, ...rest } = $props();
let pressed = $signal(0);
</script>
<button onClick=(() => pressed++) {...rest}>{label}</button>
<Button label="Save" onClick=(prevent(save)) />
- The caller's handler runs in the caller's component, as if the caller had put it on the element.
- A spread passes on handlers on the server and in markup that the browser renders, such as a new item of a
#for. - A function that the component makes itself, such as one that calls
rest.onClick, goes through a spread only when$handlermarks it. See below.
Handlers that a script makes
A function in a script is not a handler until markup uses it, so a spread leaves it out. $handler(fn, ...args) from brumate/signals marks it as one. A function that makes the attributes of a button can then give its handler too:
The sample runs when it comes into view.
indexis an argument: it goes with the handler, and the handler runs asremoveAt(index, event). Arguments are JSON values.- The handler reads the values of the component, such as
items, as a handler in markup does. A value of the function around the call, such asindex, must be an argument. preventapplies before the code of the handler loads, as in markup.- A module function can call
$handlertoo, so a library can give attributes with their handlers.
children
Markup between the tags of a component is its children prop. Insert it with {children}:
The sample runs when it comes into view.
- The children render where the component inserts them, so they read the contexts that the component provides. A
Tabpassed into aTabsreads the context of theTabs, as ifTabsrendered it. - Their values and handlers stay the caller's:
{count}in the children is the caller'scount. - The server renders the children with the page, also when the component does not insert them yet.
- When the component inserts them later, such as when the
#ifopens, they show the current values. Their event handlers work.
Templates as props
A #template is a piece of markup with parameters. Pass one as a prop to let a component render your markup for its own data:
The sample runs when it comes into view.
- The component can call the template in markup that changes in the browser, such as the items of a list, or a selected item. The template renders with the code of the component that wrote it.
- A call again at the same place, with new arguments, updates the markup in place, as a snippet of Svelte does. Its elements stay, and so does focus in them.
- The template reads the values of the component that wrote it, and its markup updates when they change.
- Its code loads in the browser with the page state, before an update needs it.
Control flow describes #template.
Promises as props
A prop can be a promise. See Promises as props.
Generic components
generics on the instance script declares type parameters, so the props of a component can depend on each other:
<script generics="T">
import { $props } from 'brumate/signals';
const { items, label } = $props<{ items: T[]; label: (item: T) => string }>();
</script>
What a component cannot do
- Signals never leave the file. A function or a child receives the value of a signal, never the signal itself. This is how the compiler sees every read and every write.
- A child cannot write a prop. It calls a function that the parent passes, as above.
- Scripts, expressions and attribute values cannot contain markup. Use a
#templatefor markup that is used as a value, and#forfor lists.
To share a value with every component inside a component, without passing it through each one, use a context.