# Blazor components

`Rask.Blazor` hosts a **real Blazor component** inside a Rask page — one from a Razor Class Library,
from MudBlazor or Radzen, or any `ComponentBase` you already have. The Razor SDK is neither replaced
nor reconfigured: it compiles `.razor` exactly as it always has, and Rask renders the result.

This is the [islands](https://rask.sh/docs/guides/islands/) contract with a different runtime behind it. A `.tsx` is a React
component rendered by React; a `.razor` is a Blazor component rendered by Blazor.

To be exact about what changed and what did not: **you still never write `.razor` to write Rask.** A
`.razor` here is a component you are *hosting* — someone else's, or your own from a class library —
not the way you author a Rask page. The chain is still the only authoring surface.

```csharp
public sealed partial class Chart : BlazorComponent<MudChart> { }
```

```csharp
Div.Class("grid")[
    H1["Revenue"],
    Chart.ChartSeries(_series).Width("100%"),
    Button.OnClick(Refresh)["Refresh"],     // ordinary Rask, right beside it
]
```

## What works, and what does not

Read this table before anything else — it is the whole shape of the feature.

| | Hosted Blazor component |
|---|---|
| Renders, styled, in the **first HTTP response** | ✅ |
| `OnInitialized`, `OnInitializedAsync`, `OnParametersSet`, `BuildRenderTree` | ✅ |
| Reacts to a Rask prop change, keeping its own state | ✅ |
| Its own `@onclick`, `@onkeydown`, `@onwheel`, … firing from the browser, with their real event args | ✅ (see [events](#events)) |
| Rask children inside it | ❌ — a compile error ([RASK062](#an-island-takes-no-children)) |
| `[Inject]` services — `IJSRuntime`, `NavigationManager`, Rask's browser APIs | ✅ (see [services](#services)) |
| `OnAfterRenderAsync` | ✅ — **once**, after the first paint (see [services](#services)) |
| `ElementReference`, and anything that takes one | ❌ — captures are discarded |
| Its own `@onsubmit`, or an event whose args are a library's own type | ❌ — see [events](#events) |
| `@bind` writing a value back | ✅ (see [binding](#binding)) |
| WebAssembly, published **trimmed** | ✅ annotated for it, but not gated end to end — see [both hosts](#both-hosts) |

## A worked example

End to end, and compiled: the component below is a real `.razor` in the repository, its island is
declared in the test suite, and the output printed here is asserted by
`tests/Rask.Blazor.Tests/DocExampleTests.cs`. A test also pins this document against the file, so the
two cannot drift apart.

**1. The Blazor component**, in a Razor Class Library — an ordinary `.razor`, compiled by the Razor
SDK, with nothing in it that knows Rask exists:

```razor
@using System.Globalization

<div class="price-tag @Tone">
    <strong>@Symbol</strong>
    <span>@Price.ToString("0.00", CultureInfo.InvariantCulture)</span>
</div>

@code {
    [Parameter, EditorRequired]
    public string Symbol { get; set; } = "";

    [Parameter]
    public decimal Price { get; set; }

    [Parameter]
    public string? Tone { get; set; }
}
```

**2. The island** — the whole declaration. Nothing is redeclared; the chain steps are read from the
component's own `[Parameter]`s:

```csharp
public sealed partial class Quote : BlazorComponent<PriceTag>;
```

There is no third step in `Program.cs`: referencing `Rask.Blazor` is the whole setup.

**3. Use it** anywhere the chain goes — a leaf, a subtree, or a whole page:

```csharp
Div.Class("grid")[
    H1["Watchlist"],
    Quote.Symbol("RASK").Price(12.5m).Tone("up")
]
```

**What comes back**, in the first HTTP response:

```html
<rask-blazor name="Quote" component="…PriceTag">
  <div class="price-tag up">
    <strong>RASK</strong>
    <span>12.50</span>
  </div>
</rask-blazor>
```

Three things in that are worth naming:

- **`Symbol` opened the chain** because `PriceTag` marked it `[EditorRequired]`. `Price` and `Tone`
  are ordinary optional steps, and omitting `Tone` leaves the component's own default alone rather
  than passing null.
- **The island is a leaf.** `Quote[ … ]` would be a compile error: an island renders the markup of
  the component it hosts and takes no Rask children — see [children](#an-island-takes-no-children).
- **`12.50` is in the first response**, not painted in later. Had `PriceTag` awaited in
  `OnInitializedAsync`, that would be finished too.

## Declaring one

Derive a `partial` class from `BlazorComponent<T>`. The base class *is* the declaration — there is no
attribute — and `T` names the component being hosted.

```csharp
public sealed partial class Chart : BlazorComponent<MudChart> { }
```

**You do not redeclare its parameters.** A Blazor component already states its surface, so the chain
steps are read from its own `[Parameter]` properties. `MudChart` declaring
`[Parameter] public List<ChartSeries> ChartSeries { get; set; }` is what makes
`Chart.ChartSeries(...)` a step.

### Required parameters

A parameter the hosted component marked `[EditorRequired]` becomes a **required** chain step — one
the call site cannot omit, taken before any optional step. Blazor already has a word for
"mandatory", so it maps onto Rask's own and neither framework has to learn the other's idiom.

```csharp
// MudBlazor: [Parameter, EditorRequired] public string Label { get; set; }
Badge.Label("New")              // ✓ required, so it opens the chain
Badge.Label("New").Tone("warn") // ✓ optional steps follow
Badge.Tone("warn")              // ✗ RASK038 — Label is never set
```

Everything not marked stays optional. An `EventCallback` is never required even when marked: "you
must handle this event" is not something the chain can usefully insist on, and an unwired callback is
simply not wired, exactly as for any other Rask component.

The type argument is how the hosted component is named — not a sibling file, the way a `.tsx` island
pairs by filename. That is deliberate: the common case has no `.razor` in your project at all, only a
referenced type.

### Renaming a step

When the hosted parameter's name is not what you want to write at the call site — or collides with a
chain entry of the same name, which is what happens for parameters called `Title`, `Label`, `Form` or
`Select` — name it explicitly:

```csharp
public sealed partial class Chart : BlazorComponent<MudChart>
{
    [BlazorParameter("ChartSeries")]
    public List<ChartSeries>? Series { get; set; }
}
```

A property you declare yourself always wins over the generated one. The name is checked: one the hosted
component does not declare as a `[Parameter]` is a compile error ([RASK100](https://rask.sh/docs/guides/diagnostics/#rask100)) rather
than a chain step that sets nothing.

### Where the `.razor` has to live

For parameters to be checked at compile time, the hosted component must come from a **referenced**
project or package. A `.razor` in the *same* project as the island is produced by the Razor source
generator, and one source generator never sees another's output — so Rask's generator cannot read its
parameters. The island still works; it just is not verified, and says so through
[RASK066](https://rask.sh/docs/guides/diagnostics/#rask066).

Move the `.razor` into a Razor Class Library and reference it. Hosting MudBlazor or Radzen — packages,
therefore referenced — is fully checked with no warning.

## It is in the first response

The component is rendered on the server, during the page render, and its markup is in the HTML that
is sent. A hosted component that awaits in `OnInitializedAsync` has **finished** before the response
goes out rather than appearing a frame later:

```csharp
protected override async Task OnInitializedAsync()
{
    Rows = await _api.Load();   // done before the page is sent
}
```

That works because the island does its rendering in `OnUpdated`, whose task is registered
in the page's quiescence scope — Rask renders, waits for outstanding work, and renders again, sending
the settled wave. See [lifecycle](https://rask.sh/docs/guides/lifecycle/).

It is also why a Blazor island suits a whole page a crawler must read. Put `[Route]` on the island and
it is the page — a `[RouteParam]` property is a step like any other and can feed a `[Parameter]`:

```csharp
[Route("/tickers/{symbol}")]
public sealed partial class TickerPage : BlazorComponent<Ticker>
{
    [RouteParam] public string Symbol { get; set; } = "";

    protected override Component? HeadAssets => Title[$"{Symbol} — quotes"];
}
```

Overriding `HeadAssets` keeps the stylesheets in `RaskBlazorOptions.HeadAssets`: they are registered on
their own, as a JavaScript island's runtime script is, so there is no `base.HeadAssets` to remember. A
route parameter that feeds a value-typed `[Parameter]` can be declared as it is (`[RouteParam] public int
Id { get; set; }`): a non-nullable value type has no "unset" state, so it is always passed on. Declare it
nullable (`int? Id`) only when the hosted component should keep its own default until a value is set.

URL attributes in that markup get the treatment a Rask element gives them: `javascript:` and `data:`
become `about:blank`, and an inline `data:image/…` survives only where it can do nothing but draw —
`src` on `img`, `audio`, `video`, `source`, `track` and `input`, a `video`'s `poster`, an SVG `image`'s
`href`. An `<iframe src="@Url">` fed a `data:` URL renders blank.

## Parameters cross as C#, not JSON

Unlike a `.tsx` island, whose props are serialized, parameters here are passed as **live CLR
objects**. `List<ChartSeries>`, a `Func<>`, your own domain type — all cross intact, and there is no
wire vocabulary to violate.

One rule worth knowing, because it is the opposite of the islands feature's: **a nullable property
that is null omits its parameter entirely** rather than passing null. `ParameterView` is
authoritative, so passing null would *overwrite* the hosted component's own default. Not specifying a
value means the component keeps whatever it would have used.

## Events

The hosted component's own event handlers work, and **no Blazor circuit is involved** — no SignalR,
no `blazor.web.js`, no second connection.

Blazor assigns a real handler id to every `@onclick` even in a static render; its own HTML writer
simply discards them. Rask walks the render tree instead and writes its own `data-rask-on-*`
attribute for each, so a click goes through the delegated listener already in the page, travels the
WebSocket that is already open, and is dispatched back into Blazor. It is the same channel the React
and Lit islands use for their callbacks.

```csharp
Table.Rows(_rows).OnRowClick(row => _selected = row)   // fires
```

**Every event a Rask element has an `On…` step for is wired, and the handler receives the event it
asked for.** The browser sends each
event's own fields — the same payload a Rask `OnKeyDown(e => …)` reads — and the island turns it into
the args type the hosted handler declares:

```razor
<input @onkeydown="OnKey" />

@code {
    void OnKey(KeyboardEventArgs e) => _last = e.Key;   // "Enter", not ""
}
```

| Blazor's args | Filled with |
|---|---|
| `MouseEventArgs`, `PointerEventArgs`, `WheelEventArgs`, `DragEventArgs` | position, buttons, modifier keys; the pointer, the deltas, the `DataTransfer` |
| `KeyboardEventArgs` | `Key`, `Code`, `Location`, `Repeat`, `IsComposing`, modifier keys |
| `TouchEventArgs` | the three touch lists, modifier keys |
| `FocusEventArgs`, `ClipboardEventArgs` | `Type` — all Blazor declares on them |
| `ProgressEventArgs`, `ErrorEventArgs` | `Type` only: Rask models these two as a plain event |
| `EventArgs`, or a handler that takes nothing | nothing is asked of the browser at all |

Two things are not wired, and both render **no attribute** rather than pretending — a component that
looks wired and does nothing on the first click is the failure this package exists to avoid:

- **An args type outside that table** — a component library's own `[EventHandler]` args. Building one
  would take reflection, which a trimmed WebAssembly publish does not survive. The island logs one
  warning naming the island, the event and the type.
- **`@onsubmit`.** A form's submit travels Rask's form channel, which the island does not bridge.

Each event is one round trip to .NET, as it is for any Rask handler. That is nothing for a click and
worth a thought for `@onpointermove`, `@onmousemove` or `@onwheel`, which fire many times a second.

## Binding

`@bind` works, including the write-back. It travels a different channel from the click above, and
that is the whole reason it works: `change` and `input` are deliberately absent from Rask's DOM-event
table, because a value-carrying event goes through Rask's **input** channel instead — the one that
ships the element's value alongside the handler id.

So a bound input renders with `data-rask-on-change` (or `data-rask-on-input` under
`@bind:event="oninput"`), the browser sends the value, Rask hands it to the island, and the island
turns it into the `ChangeEventArgs` the binder `@bind` generated is waiting for.

```razor
<input @bind="Text" />
<p>echo: @Text</p>
```

Typing updates `Text` inside the hosted component and the echo follows, with no circuit involved.

Blazor's binder casts rather than parses, so the value arrives in the type it expects: a string for a
text input or a single select, a `bool` for `<input type="checkbox">`, a `string[]` for
`<select multiple>`.

```razor
<input type="checkbox" @bind="Done" />
<select multiple @bind="Tags">…</select>
```

One limit: `@bind:event="oninput"` on a checkbox or a multi-select still receives a string, because
only the `change` frame carries the checked state and the whole selection. Leave those two on the
default event.

## An island takes no children

An island renders the markup of the component it hosts, and nothing else. Writing children into one is
a **compile error** — [RASK062](https://rask.sh/docs/guides/diagnostics/#rask062) — rather than something that binds, compiles
and then silently renders nothing:

```csharp
Chart.Series(_series)[ H2["Revenue"] ]   // RASK062
```

Children would have to cross as a `RenderFragment`, and there is no answer that is right for every
component. The hosted type may have no fragment parameter at all, one under a name only it knows
(`Content`, `Body`), or several of them (`HeaderContent`, `ToolBarContent`, `RowTemplate`). Picking
among those is how an island ends up looking composable while quietly dropping what it was given —
and a silent drop is the one failure this package is built to avoid.

Compose the other way round instead. It costs nothing, and it reads better:

```csharp
Div.Class("rounded-xl border p-4")[
    H2["Revenue"],
    Chart.Series(_series),
    Button.OnClick(Refresh)["Refresh"],
]
```

The Rask markup stays Rask's — ordinary elements, ordinary handlers, an ordinary diff — and the island
stays a leaf that owns exactly the DOM its hosted component wrote.

> A JS island draws the line in a different place: it takes children of its own runtime — a React
> island takes React islands and text — because those travel in its props and its framework renders
> them. Rask markup stays out of both families, for the same reason — see
> [islands.md](https://rask.sh/docs/guides/islands/#children).

## Tailwind

Worth knowing before you write a utility class in a hosted component, because the failure is silent:
**Tailwind only emits the classes it can see**, and it detects sources from the project directory it
runs in. A Razor Class Library is a different project directory, so the app's Tailwind never reads it
— and a component written in `flex gap-4` renders with classes no stylesheet defines.

A `.razor` in the **same project** as the island is already inside the scanned directory and needs
nothing. For a class library, pick one:

**The library compiles its own stylesheet.** Give it a `Styles/app.css` with `@import "tailwindcss";`
and it compiles on its own build, shipping the result as a static web asset at
`_content/<PackageId>/css/app.css` — which the app links once.

```csharp
services.AddRaskBlazor(o => o.HeadAssets.Add(
    Link.Rel("stylesheet").Href("_content/MyComponents/css/app.css")));
```

**Or the app scans the library.** Tailwind v4 takes extra sources as a directive, so one stylesheet
can cover both projects — no second compile, no `_content` link:

```css
@import "tailwindcss";
@source "../MyComponents";
```

The first keeps the library self-contained and shippable on its own; the second keeps one stylesheet
for the whole app. Neither is more correct, and a third-party library (MudBlazor, Radzen) needs
neither — it ships its own CSS, which you add through `HeadAssets` the same way.

## Services

There is nothing to register. An island supplies what a library component demands and an app has no
reason to provide — most notably `NavigationManager`, which many components inject and throw without —
and steps aside for anything the app registers itself. `AddRaskBlazor(o => …)` exists only to change
an option.

**`NavigationManager` is Rask's own routing.** `Uri` is the page on screen under the app's path base,
so a hosted `NavLink`, breadcrumb or tab strip highlights the right entry; `NavigateTo` from a hosted
handler moves the browser exactly as `Go.To` does; and a navigation Rask makes raises
`LocationChanged`. Rask's route carries no scheme or host, so those come from
`RaskBlazorOptions.BaseUri` (`http://localhost/` unless you set it) — set it if a hosted component
prints absolute URLs. `NavigateTo` to another site, or with `forceLoad`, only updates `Uri`.

**`[Inject]` works, and resolves out of your app's own container.** The island builds its hosted
component through Blazor's own activator, so anything you registered is available — `IJSRuntime`,
your own services, and Rask's typed browser APIs:

```csharp
@inject IViewTransitions ViewTransitions

@code {
    private bool _animated;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;
        _animated = await ViewTransitions.IsSupported();
    }
}
```

This did not work before [#956](https://github.com/pal-tamas/rask/issues/956): the component was
constructed with `new()`, which skips the injection path entirely, so every `[Inject]` property was
null until the first use threw somewhere that named nothing. If you want to control construction
yourself, register an `IComponentActivator` — Blazor's own seam, which the island honours.

**`OnAfterRenderAsync` fires once, after the island's first paint.** Rask drives it, because
`StaticHtmlRenderer` never does. Once rather than after every render, deliberately: a Rask render
walk is not a Blazor render — the island is walked whenever anything on the page changes — so "after
every render" would fire far more often than a `.razor` author expects, for reasons that have nothing
to do with the component. Use `OnParametersSet` to react to later prop changes.

Calling `StateHasChanged` from the hook is safe. The island holds back the repaint a hosted component
asks for while its hook is running and publishes it once when the hook returns — and because the hook
fires once, that repaint cannot feed it again. So the usual shape works: read what you need, assign
it, call `StateHasChanged`.

`IJSRuntime` only falls back to a runtime that **throws with a message naming the fix** when the app
registered none of its own. Both hosts register a real one, so in practice you get that.

**What still does not work is `ElementReference`.** `BlazorFrameWriter` discards element-reference
captures, so `JS.InvokeVoidAsync("thing.init", elementRef)` — the shape most component libraries use
to bootstrap themselves against their own JavaScript — has no reference to pass. Interop that does
not need one works.

## Both hosts

`Rask.Blazor` targets `net10.0` **and** `net10.0-browser`, and the two share one code path with no
`#if`: a hosted component is rendered to markup in process, which browser-WebAssembly does as readily
as a server. The only difference is where the renderer comes from — the ASP.NET shared framework on
the server, the `Microsoft.AspNetCore.Components.Web` package in the browser.

**Trimmed, which is the default a WASM app publishes with.** That took an annotation, and the reason
is worth stating: `[Parameter]` discovery reflects inside
`Microsoft.AspNetCore.Components`, on the hosted type — so the trimmer removes the very property
setters `ParameterView` is about to call, and **nothing reports it**. The trim analyser has nothing to
point at (the reflection is in someone else's assembly), the build stays green, no exception is
thrown, and the island renders as an empty `<rask-blazor>` element. `BlazorComponent<TComponent>`
therefore annotates its type parameter:

```csharp
public abstract partial class BlazorComponent<
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>
```

which moves the requirement to the one place that knows the concrete type — your island's own
declaration — and the trimmer keeps the hosted component's members in whatever assembly it lives in.
`All` rather than only the public properties, because `[Inject]` reaches non-public properties too and
Blazor's own activation path asks for exactly that. Nothing to configure.

What that does **not** cover is a component library reaching for members by name beyond its
parameters — reflection Rask cannot see and cannot annotate for. If a hosted component from a large
library renders wrong only after publishing, confirm it by publishing once with:

```xml
<PublishTrimmed>false</PublishTrimmed>
```

and, if that is the difference, root the library with a `TrimmerRootAssembly` rather than turning
trimming off for the whole app.

**The trimmed path has no gate in this repository.** It used to: the WASM showcase hosted a real
`.razor` from a Razor Class Library on a **Blazor island** page, published trimmed on every build, and
a browser E2E checked the hosted component's *output* — its parameters, its own `@onclick`, its
`@bind` — because an empty island is exactly what a "the element is there" check would pass on. Both
projects were deleted with the rest of the samples. The site cannot carry a replacement without a
second project: a `.razor` in the same project as its island is [RASK066](https://rask.sh/docs/guides/diagnostics/#rask066), a
warning, and this repository builds with warnings as errors. What remains is
`TrimmingContractTests`, which pins that the annotation is still on the type parameter — not that a
trimmed app renders. If you host a Blazor component in a trimmed publish, verify it renders content
yourself; a green build proves nothing here.

It is deliberately **not** among the batteries `Rask.Server` or `Rask.Wasm` bring. Everything there is
referenced by every app on that host, and an app that wants nothing to do with Blazor should not
carry its renderer.

## What is not here yet

The Blazor features a hosted component may reach for, and where each stands:

| Blazor feature | In an island | Why |
|---|---|---|
| A cascading value supplied from Rask | not addressed | Only `[Parameter]`s become chain steps, and nothing maps Rask's `Context.Provide` onto a `[CascadingParameter]`. |
| `<PageTitle>`, `<HeadContent>` | not supported | They render into a `HeadOutlet`, and the page has none — the `<head>` is Rask's. Set the title from the Rask page. |
| `<Virtualize>` | not supported | It drives its spacers from Blazor's own JavaScript through `ElementReference`, and neither is there. |
| Element `@ref` / `ElementReference` | not supported | The frame writer discards the capture, so the reference never points at an element. |
| `RenderFragment`, `ChildContent`, `RenderFragment<T>` | not supported | No chain step is generated, and children are [RASK062](https://rask.sh/docs/guides/diagnostics/#rask062). |
| A circuit, `@rendermode` | not supported | There is no circuit and no `blazor.web.js`; the island's renderer resolves no render mode. |

- **`ElementReference`, and interop that needs one.** Element-reference captures are discarded by the
  frame writer, so a component that hands its own JavaScript a reference to bootstrap against — which
  is how most menus, dialogs and autocompletes work — still renders inert. `[Inject]`,
  `IJSRuntime` and `OnAfterRenderAsync` do work now; see [services](#services).
- **A prop change replaces the island's DOM.** The update ships as a subtree replace rather than a
  fine diff, so scroll position and text selection inside the island are lost when a prop changes.
- **No `RenderFragment` parameter gets a chain step** — not `ChildContent`, not a named one, not a
  templated `RenderFragment<T>`. See [children](#an-island-takes-no-children).
- **`@onsubmit`, and an event whose args are a library's own type**, emit no attribute. See
  [events](#events).
- **Circuit mode.** There is none: a hosted component never gets a Blazor circuit of its own.

## Diagnostics

[RASK061](https://rask.sh/docs/guides/diagnostics/#rask061) · [RASK062](https://rask.sh/docs/guides/diagnostics/#rask062) · [RASK064](https://rask.sh/docs/guides/diagnostics/#rask064) ·
[RASK066](https://rask.sh/docs/guides/diagnostics/#rask066)

## Why `.razor` is not compiled into the chain

A reasonable thing to want: lower `.razor` into Rask's own chain, so a Razor-authored component is an
ordinary Rask component with the full live diff and WebAssembly support. It was investigated and
rejected on facts, not taste:

- **Razor's syntax layer is `internal` in every shipped version** — `RazorSyntaxTree.Root` returns a
  type no third party can name. There is no public way to walk a parsed Razor document.
- **The .NET 10 SDK's Razor compiler is closed.** It exposes neither the intermediate document nor a
  way to register a pass that would receive one. Its 23 `InternalsVisibleTo` friends — `rzc`, Blazor's
  source generator, the VS and VS Code Razor extensions — reach those instead.
- **There is no open version to pin.** The last published `Microsoft.AspNetCore.Razor.Language` is
  6.0.36, a .NET 6-era package out of support since November 2024.

Hosting the SDK's own output costs nothing that can rot, and gains every Razor feature for free —
including ones a hand-written front end would have taken years to reach.
