brumate

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>

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:

Edit the code to run it again

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;
}>();

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.

Edit the code to run it again

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.

Edit the code to run it again

The sample runs when it comes into view.

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>

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:

Edit the code to run it again

The sample runs when 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:

Edit the code to run it again

The sample runs when it comes into view.

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)) />

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:

App.brmEdit the code to run it again

The sample runs when it comes into view.

children

Markup between the tags of a component is its children prop. Insert it with {children}:

Edit the code to run it again

The sample runs when it comes into view.

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:

Edit the code to run it again

The sample runs when it comes into view.

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

To share a value with every component inside a component, without passing it through each one, use a context.