# generic component

> A component with type parameters, bound at each call site from the arguments — one component that works over any enum or any row type, instead of a near-identical copy per type.

<!-- id: ui-generic-component · area: ui · stability: preview · html: https://osysharp.com/reference/ui/generic-component/ -->

## Summary        {#summary}
A `component` may declare type parameters. They are **inferred at each call site** from the arguments — there is no
constraint to declare and no type to pass — so one `Dropdown<TEnum>` serves every enum in the app, and one
`Table<TRow>` serves every row type. Inside the body a type parameter is a real type: it types parameters, action
parameters and locals, and `TEnum.Members` reads the members of whichever enum the call site bound.

## Signature      {#signature}
```osy syntax
component Name<T>(Binding<T> value) { … }        // T inferred from the bound field's type
component Name<T>(T[] rows) { … }                // T inferred from the collection's element type
component Name<T>(T item) { … }                  // T inferred from the argument's type

component Name<T>(Binding<T> value) where T : enum { … }   // …or state the constraint outright

Name(value: org.Status)                          // the call site says what T is
```

## Description    {#description}

### The type parameter is inferred at the call site   {#inference}
A call site already names the type: `Dropdown(value: org.Status)` can only mean `OrganizationStatus`. So there is no
explicit type argument to pass. A parameter binds a type parameter when it is declared as `T` (from the argument's own
type), `T[]` (from the collection's **element** type) or `Binding<T>` (from the bound field's type). The first
parameter that names a given type parameter wins; a later one is checked against that binding.

**What the type parameter must BE** is normally inferred too, from what the body does with it: a body that reads
`T.Members` has said that `T` is an enum, and every call site is checked against that. You do not have to write it
down — see *Declaring the constraint* below for the one case where you do.

### Declaring the constraint — `where T : enum`   {#constraints}
Inference works because the compiler can see the body. **A component you did not declare in this app — one from a
kit or capability — is compiled separately, and its body is not re-read here.** So there is nothing to infer from,
and a call to it is refused rather than guessed at:

> `'Dropdown' is generic, but nothing here says what 'TEnum' has to be…`

Guessing "unconstrained" would be worse than refusing: the body would ask for the members of something that is not
an enum, get none, and render an empty control that looks live and does nothing.

A **declared** constraint fixes this, because it lives on the declaration rather than in the body, and the
declaration is what crosses into your app:

```osy title="a generic control a kit can ship" syntax
component Dropdown<TEnum>(Binding<TEnum> value) where TEnum : enum {
  render {
    foreach (var m in TEnum.Members) { Text(m.Label); }
  }
}
```

`enum` is the only constraint kind. Write `where` once per constrained parameter, after the parameter list:
`component Pair<A, B>(A a, B b) where A : enum where B : enum`.

**Rule of thumb:** if the component lives in your own app, you never need `where` — inference covers it. Declare it
when you ship a component for other apps to call and they cannot see its body.

⚠ **A declared constraint is unconditional**, which is exactly what you want for a component that is always over an
enum — and exactly what you must not write for one that is only *sometimes* over an enum. See the next section.

### A constraint that only applies when you fall back   {#fallback-constraints}
A **fallback** — a parameter's default value, or a slot's default body — is what the component does *instead of*
something you may supply. So a demand it raises binds only the call sites that actually reach it:

```osy syntax
component Dropdown<T>(Binding<T> value, T[] options = T.Members) {
  render {
    foreach (var o in options) { Slot(o) { Text(o.Label); } }
  }
}

Dropdown(value: order.Status)                                       // T = OrderStatus — falls back, so T must be an enum
Dropdown(value: project.Lead, options: people) { p => Text(p.Name); }   // T = User — supplies both, so it need not be
```

Both defaults above say the same thing: *if you name no options, and no template, this is an enum picker*. Neither
says *T is always an enum* — and reading them that way would refuse the second call, over a constraint neither of its
arguments touches.

The two are suppressed by different things, so they narrow separately:

| Fallback | Applies to a call site that… |
|---|---|
| `T[] options = T.Members` | passes no `options:` argument |
| `Slot(o) { … }`'s default body | writes no template for that slot |

Anything the body does **outside** a fallback still binds every call site, however much it supplies. And a slot whose
name is computed (`Slot(col.Key, row)`) cannot be matched against a call site's fills, so a demand from its default
body stays unconditional — an escaped demand would render the empty, live-looking control this check exists to refuse.

⚠ **This is why the kit's `Dropdown` declares no `where` clause.** It is one component over an enum *or* over any
rows, and its enum-ness lives entirely in those two fallbacks; a declared `where T : enum` would re-refuse every
collection-backed call.

### What a type parameter can do inside the body   {#in-the-body}
It behaves as a type, not as an escape hatch — the body is fully checked:

| Written | Means |
|---|---|
| `Binding<T> value` | a two-way binding over a `T`, exactly like `Binding<OrgRole>` |
| `action Choose(T m)` | an action taking a `T` |
| `value = m` | assignment, when both are the same `T` |
| `m == value` | equality, when both are the same `T` (two *different* type parameters are not comparable) |
| `T.Members` | every member of the enum `T` was bound to (see below) |
| `m.Label` / `m.Description` / `m.Name` | the enum's words ([[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/)), read off a `T`-typed value |
| `Icon(m)` | the glyph the member declares with `[Icon(…)]` |
| passing `m` where a tone enum is expected | the tone the member declares with `[Tone(…)]` |

Anything else off the bare name — `T.Anything` — is a compile error: a type parameter has no members of its own.

### `T.Members` and when it is an enum   {#enum-constraint}
Reading `T.Members` (or a word off a `T`-typed value) is what makes `T` an **enum** — that use IS the constraint. Every
call site is then checked, and binding something that is not an enum is a compile error naming the component, the
parameter and the type it got:

> `'Dropdown' reads `TEnum.Members`, so 'TEnum' has to be an ENUM — but 'value' here is string.`

A type parameter the body never uses as an enum — `Table<TRow>(TRow[] rows)` — binds to anything.

### Decoration comes from the model, not from the component   {#decoration}
`Icon(m)` renders the `[Icon(…)]` the member declares, and passing `m` where a tone enum is expected renders its
`[Tone(…)]` — the same two spellings that already work on a concrete enum value, now reaching a type parameter. So a
generic component decorates every enum without naming a single member.

Both carry a completeness demand to the call site, because a half-decorated enum would render a **blank glyph** or an
unstyled row — something that looks deliberate and carries none of the meaning the model declared. Binding an enum
whose members do not all declare what the body renders is a compile error naming the members that don't:

> `'Dropdown' renders each member's [Icon(…)], so EVERY 'Bare' member needs one — 'NoIcon' does not.`

This is the same check the concrete spelling makes; it simply moves to the call site, which is the only place that
knows which enum is in play.

The list is the same one `<Enum>.Members` gives for a concrete enum, in declaration order, and the values are the same,
so `m == value` and `m.Label` behave identically either way. The difference is **when**: a concrete `Status.Members` is
expanded by the compiler, while `T.Members` is read at render time from the enum this particular instance was bound to.
That is what lets one component body serve two call sites over different enums on the same page.

### One body, many call sites   {#one-body}
A generic component is compiled **once**. The type each call site inferred travels to that instance, so two instances
of one component can list two different enums side by side. Nothing is duplicated per type: adding a fifth enum-backed
picker to an app adds a call, not a component.

## Examples       {#examples}

One dropdown, two enums, on one page. Both instances are the same component; each lists its own enum's members and
writes the chosen one back through its binding.

⚠ **The `Dropdown` below is THIS PAGE'S, declared above — not the kit's.** The name is deliberate (a generic control
is what the kit ships one of), and the shape is cut down to the one thing being taught: type inference. The kit's own
control takes a required caption first — `Dropdown("Status", value: order.Status)` — see [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/).

```osy title="one dropdown for every enum" test app=ui-generic-component
enum Tone { Neutral, Accent }

enum Status {
  /// Open for business
  [Tone(Tone.Accent)]  [Icon(check)] [Label("Active")] Active,
  /// Temporarily closed
  [Tone(Tone.Neutral)] [Icon(close)] [Label("Suspended")] Suspended,
}

enum Role {
  [Tone(Tone.Accent)]  [Icon(check)] [Label("Owner")] Owner,
  [Tone(Tone.Neutral)] [Icon(close)] [Label("Member")] Member,
}

// One option row. `tone` is an ordinary enum-typed param — passing a member of ANOTHER enum coerces to the tone that
// member declares, which is what lets the generic body above decorate without knowing the enum.
[Composable] component Option(string label, Tone tone) {
  // Literal colours here only because this example declares no `theme` — a real app uses its declared tokens.
  variants { base { Px = 1; } tone { Neutral { Bg = "#f6f6f7"; } Accent { Bg = "#e8f0fe"; } } }
  render { Row(gap: 1) { Slot; Text(label); } }
}

// `[Composable]` for the same reason `Option` above has it: the page below is PUBLIC, and an anonymous visitor can
// only load a component that says it carries no auth identity of its own ([[Composable] — presentational components in public pages](https://osysharp.com/reference/ui/composable/)). Its data reads, if it
// had any, would still be gated.
[Composable]
component Dropdown<TEnum>(Binding<TEnum> value) {
  bool open = false;
  action Toggle() { open = !open; }
  action Choose(TEnum m) { value = m; open = false; }

  render {
    Box(position: Position.Relative) {
      Pressable(onClick: Toggle) { Text(value.Label); }
      if (open) {
        Stack(gap: 0) {
          foreach (var m in TEnum.Members) {
            // Every option's glyph and tint comes from the member's own declaration — nothing here names one.
            Pressable(onClick: () => Choose(m)) { Option(m.Label, m) { Icon(m, size: 14); } }
          }
        }
      }
    }
  }
}

[Page("/settings")]
[AllowAnonymous]
component SettingsPage() {
  Status status = Status.Active;
  Role role = Role.Member;
  render {
    Stack(gap: 2) {
      Dropdown(value: status);
      Dropdown(value: role);
    }
  }
}
```

A type parameter that is never used as an enum takes any type — this is the row-type case, and it is checked the same
way (the datum in the template binds to `T`).

```osy title="a generic row list" syntax
component Table<TRow>(TRow[] rows) {
  render { Stack(gap: 1) { foreach (var r in rows) { Text("row"); } } }
}

Table(rows: expenses)     // TRow = Expense
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the component archetype these extend
- [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — `[Label]` / `[Icon]` / `[Tone]`, and the `Label` / `Description` / `Name` words a member carries
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring the enums a generic component is bound to
