You're reading the Rask v1.0.0 guides.View source
All guides

Building components — the chain

Markup in Rask is C#. You name a component and chain onto it; there is no new, no factory call, and no using to remember.


Div.Class("panel")[
    H2.Class("mb-1 text-lg font-semibold")["Products"],
    P["Everything we sell."]
]

The […] is an indexer, not a collection initializer, so the last child takes no trailing comma.

The name is the component. Div is a Div, so . shows every property it has — its own and the whole inherited HTML surface. Children go in the indexer.

Components that need something first

Some components cannot exist until you have told them something. A form control does not know what type it binds until you say; a toast has no message until you give it one. Those properties are steps rather than setters, and the chain asks for them first:


BsToast.Id(7).Message("Saved").Delay(3000)

Id and Message are required, so they come first — in either order. Everything optional follows. Miss one and there is nothing to render: the component does not exist yet, so the mistake is a compile error at the point you made it, not a null at runtime.

Bound and controlled

A form control is either bound to a model expression or controlled by a value you hold. You choose at the first step, and the choice is the type:


Input.Bind(() => _form.Name).Validate(ProductName.Check).Id("name")   // bound
Input.Value(_text).OnChange(v => _text = v)                           // controlled

Having picked one, the other mode is not offered — not the opening step, and not the steps that belong to it. A bound chain has Validate and the AfterBind hooks; a controlled one has Checked and the OnInput/OnChange callbacks; neither can reach the other's. A control bound to an expression and handed a value has two sources of truth and nothing decides which wins — and one told to bind and given an OnInput is worse, because bound mode installs its own write-back and the handler simply never runs. So the surface does not let you write either.

Everything that belongs to neither mode — Placeholder, Type, Required, OnFiles, the whole Class/Id/Aria element surface — stays reachable from both.

Both spellings infer the type from what you passed, so Input<string>() is never needed. Where the value alone cannot say — null names no type — write it once:


Input.Value<string>(null).Placeholder("Anything")

Two things to settle

A few components need more than one fact before they exist. UiSelect binds a value and offers options:


UiSelect.Bind(() => _m.Country)                       // T — what the model holds, and the mode
        .Options([("hu", "Hungary"), ("gb", "UK")])   // the values and the words shown
        .Label("Country")

The opening step is the one that pins the type argument, and for a form control it fixes the mode with it: Bind opens the bound chain, Value the controlled one, and the two are mutually exclusive because a control with both would have two sources of truth for one field. Everything else — Label, Options, Placeholder — follows in any order, because none of them says anything about T.

That is a language constraint rather than a house rule: a step whose type mentions T cannot be written before something has said what T is.

What the IDE shows you

  • Typing a component name and . on an ordinary component lists every setter it has.
  • On a component with something outstanding, it lists only what is still missing — which is the answer to "how do I start?" rather than a hundred properties you cannot use yet.
  • Once nothing is outstanding you get the component, and with it the full surface and the […] indexer.

The intermediate types you may glimpse — RaskSeed_…, RaskStage_…, RaskPending_… — are generated machinery. They are hidden from completion and never written by hand.

Callbacks

Callbacks are ordinary properties, set like any other:


Button.OnClick(Save)["Save"]
BsToast.Id(1).Message("Saved").OnClose(() => _open = false)

A callback property on a component you write is an ordinary delegate — nothing to wrap, nothing to learn:


public Action? OnPick { get; set; }
public Func<Task>? OnSaveAsync { get; set; }
public Action<int>? OnRate { get; set; }
public Func<Product, Component>? Template { get; set; }

The chain's receiver is Build<TComponent> rather than the component, so .OnPick(fn) resolves to the setter and not to invoking the property — which is what a delegate-typed property on the receiver would have meant (CS1593). Call one back the way you call any delegate: OnPick?.Invoke().

Your own components

Nothing above is special to the framework's components. A component you write gets the same surface:


public sealed partial class ProductCard : Component
{
    public required string Title { get; set; }   // a step
    public string? Subtitle { get; set; }        // a setter
    public Action? OnPick { get; set; }

    protected override Component? Render() => …;
}

ProductCard.Title("Coffee").Subtitle("Dark roast").OnPick(Pick)

A non-nullable property with no initializer is required — the same rule RASK001 describes — so it becomes a step. Give it a nullable type or an initializer if it is genuinely optional.

Lists of components

A chain that ends at the [...] children indexer is already a component, so projecting one has always worked:


Tbody[rows.Select(r => Tr.Key(r.Id)[Td[r.Name], Td[r.Total]])]

A chain that ends at a step is different. OpsBadge.Key(k).Label(v) has the type Build<OpsBadge>, and the implicit conversion that makes it a component at a call site does not lift through IEnumerable<> — so a projection of those is not a sequence of components. The children indexer accepts it anyway:


Div[scopes.Select(s => OpsBadge.Key(s.Key).Label($"{s.Key}={s.Value}"))]

Literals and a projection can also sit in one list, which saves a Concat:


Div["Showing ", rows.Select(r => Row.Key(r.Id).For(r)), " of ", total]

Nested sequences flatten, so SelectMany is optional, and a sequence of plain values renders as text exactly as a literal child does.

This last overload takes object?, so it is the one place in the chain where a mistake is not a compile error: an element that is neither a component, nor a chain, nor a value with a text representation throws while rendering, naming the type. A generic indexer cannot express the typed version — C# forbids generic indexers, and a C# 14 extension block cannot declare one either (CS9282).

Keys still matter. RASK022 reads a chain that ends at a step, so a list item without .Key(…) is reported in every one of the shapes above.

See also

  • Composition — context, callbacks, and passing components around.
  • Forms — binding, validation, and the form controls in full.
  • Diagnostics — RASK001 and RASK038, which are the rules above stated as errors.