Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / UI

Slot(item) — let the caller decide what each row looks like

foreach (var o in options) { Slot(o); }

`Slot(item)` renders the caller's template once for that item. It is how a component owns the list — the layout, the scrolling, the selection — while the app that uses it owns what a single row looks like.

preview2 examples compiled by CIuicomponentslotgeneric

Summary#

A plain Slot places the caller's content once. Slot(item) renders it once per item, with a different value each time — so one component can serve a list of anything.

Signature#

// in the component — pass the item
Slot(<value>);

// at the call site — receive it
SomeComponent(items: xs) { x => … }

Description#

Some components are about a collection: a picker, a list, a table. The component knows how to lay the collection out; only the app knows what one row should say. Slot(item) is what connects those.

The component passes the item:

[Composable] component Picker<T>(T[] options) {
  render {
    Stack {
      foreach (var o in options) { Slot(o); }
    }
  }
}

The caller supplies a template, naming the value it receives:

Picker(options: customers) { c =>
  Row { Text(c.Name); Hint(c.Region); }
}

That block is a template, not content: it runs once per Slot(o), and c is a different customer each time.

The template still sees everything around it. Only the named value comes from the component; the rest of the expression resolves where you wrote it, so a page's own state and the item can appear together:

string search = "";

Picker(options: customers) { c =>
  Text(c.Name, bold: c.Name == search);     // `c` from Picker, `search` from the page
}

A plain Slot is unchanged. A component that just wraps its caller's content — a card, a panel, a dialog — writes Slot; exactly as before. You only need the argument when the same content has to render more than once with different values.

A component may use both. Slot(current) for the closed state of a dropdown and Slot(o) inside its list both render the same template, with different values — which is usually what you want, since the selected row should look like the rows it was chosen from.

A repeated slot needs a template, and the compiler says so#

A fill is placed, not copied. The caller builds its content once and the component moves it to wherever the Slot is — so a Slot reached once per row can only ever end up holding it in the last row, and every earlier row renders empty. Both halves of that mismatch are compile errors rather than a blank page:

[Composable] component Rows<T>(T[] rows) {
  render { foreach (var r in rows) { Stack { Slot("cell"); } } }
}

Rows(rows: xs) { slot cell { Text("hi"); } }
// ✗ 'Rows' renders `Slot("cell")` inside a `foreach`, so this fill would be placed once per row …

The fix is two-sided, and so is the message: the caller writes a template, and the component gives that template a datum to render with. A template is only ever invoked by Slot(<name>, <datum>), so adding r => alone swaps a last-row-only page for an empty one — which is refused too, naming the component's half.

[Composable] component Rows<T>(T[] rows) {
  render { foreach (var r in rows) { Stack { Slot("cell", r); } } }   // ← hand each invocation its row
}

Rows(rows: xs) { slot cell { r => Text(r.Name); } }                   // ← receive it

Two mutually exclusive if arms are not repetition. A component that writes the same Slot in a wide arm and a narrow one renders one of them at a time, so plain content is correct there and is accepted.

If a caller passes plain content to a component that expects a template, nothing renders at that slot — the same as any unfilled slot. Slots are optional by design.

Examples#

A picker that owns the list and lets its caller own the row:

entity Customer { [MaxLength(100)] string Name; }

[Composable] component Picker<T>(T[] options) {
  render {
    Stack {
      foreach (var o in options) { Slot(o); }
    }
  }
}

component Home() {
  live var rows = Customer.ToList();
  render {
    Picker(options: rows) { c =>
      Text(c.Name);
    }
  }
}

A named per-row cell — the shape the refusals above steer to:

entity Customer { [MaxLength(100)] string Name; }

[Composable] component Rows<T>(T[] rows) {
  render {
    Stack {
      foreach (var r in rows) { Row { Slot("cell", r); } }
    }
  }
}

component Home() {
  live var cs = Customer.ToList();
  render {
    Rows(rows: cs) { slot cell { c => Text(c.Name); } }
  }
}

See also#

Related

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

[Composable] — presentational components in public pages

Mark a presentational, composition-only component `[Composable]` so a public page can compose it without marking it…