# Forms & validation

Rask binds inputs two-way with a strongly-typed `Bind` expression, validates each field by the rule
written beside it — `.Validate(…)` — and tracks per-field state (touched / modified / messages /
in-flight validation) on an `EditContext`. The same component code runs server-rendered or on WASM.

This guide builds up in layers: binding → forms → inline validation, sync and async → nested models →
radio/checkbox groups. DataAnnotations and FluentValidation are supported too, for a model that already
carries them.

For the analyzer IDs referenced here (`RASK001`, `RASK022`, …) see [diagnostics.md](https://rask.sh/docs/guides/diagnostics/).

## On this page

- [Validation](https://rask.sh/docs/guides/forms-validation/) — inline `.Validate(…)` on a field or the form, async rules, rules in a value object; then DataAnnotations and FluentValidation.
- [Nested models & control groups](https://rask.sh/docs/guides/forms-advanced/) — complex models, radio/checkbox groups, custom controls.

---

## 1. Two-way binding

The low-level path wires `Value` and an event handler yourself:

```csharp
Input.Value(_typed).Type(InputType.Text).OnInput(v => _typed = v)
P[$"Echo: {_typed}"]
```


The ergonomic path is a `Bind` expression — one call replaces `Value` + `OnInput` + parsing:

```csharp
Input.Bind(() => _model.Name).Live().Placeholder("Your name")
P[$"Hello, {_model.Name}!"]
```

`.Live()` is there because the page reads the name back while it is typed. A bound field that is only read
when the form is saved does not need it — see [bind timing](#bind-timing).


`Bind` is an `Expression<Func<TProp>>`. The chain step reads the expression and
derives everything from the bound property:

- **Input name** ← the property name (`name="Name"`). Override with `Name:`.
- **Input type** ← the property's CLR type (`BindingHelpers.DefaultInputType`):
  `bool → checkbox`, every numeric primitive `→ number`, `DateOnly → date`,
  `DateTime`/`DateTimeOffset` `→ datetime-local`, `TimeOnly`/`TimeSpan` `→ time`, everything else
  `→ text`. Override with `Type:` — an `InputType` enum value. The full set is
  `Text`, `Search`, `Tel`, `Url`, `Email`, `Password`, `Number`, `Checkbox`, `Radio`, `File`, `Range`,
  `Color`, `Date`, `DatetimeLocal` (renders `datetime-local`), `Time`, `Week`, `Month`, `Hidden`,
  `Button`, `Submit`, `Reset`, `Image`. The *string-only* family (`Text`/`Search`/`Tel`/`Url`/`Email`/
  `Password`) only makes sense on an `HTMLInputElement<string>`; setting one on a non-string bound input is
  [RASK025](https://rask.sh/docs/guides/diagnostics/#rask025).
- **Passwords are the browser's.** An input of `type=password` (or `autocomplete` `current-password`,
  `new-password` or `one-time-code`) sends what was typed to the model and never gets it back: the server writes
  `""` to clear it and nothing else, so a model value set from the server does not reach the field, and a page
  that prefills a password is a bug in the page.
- **Update timing** — a bound control says nothing until the next action, and its value travels with
  that: see [bind timing](#bind-timing).

### Bind timing

**A bound control waits for the next action.** Typing into it, choosing in it and leaving it send nothing,
run no rule and render nothing. When the reader presses a button, submits, or does anything else the page
hears, everything they entered is sent first, in order, and then the action. This is how Livewire's
`wire:model` works, and Flux's forms with it.

Three steps after `Bind` say otherwise:

```csharp
Input.Bind(() => _m.Name)                                    // waits: the value travels with the next action
Input.Bind(() => _m.Title).Live()                            // as it is typed, after a 150 ms pause
Input.Bind(() => _m.Code).Live().Debounce(300.Milliseconds)  // a pause of your own
Input.Bind(() => _m.Notes).Blur()                            // on leaving the field
Select.Bind(() => _m.Country).Live()                         // as soon as it is picked
```

| Step | Livewire | Writes the model and runs the rules |
| --- | --- | --- |
| *(none)* | `wire:model` | with the next action |
| `.Live()` | `wire:model.live` | as the reader types, 150 ms after the last key; at once for a checkbox, a radio, a select, a range |
| `.Debounce(300.Milliseconds)` | `wire:model.live.debounce.300ms` | as the reader types, after that pause. Implies `.Live()`; `TimeSpan.Zero` sends every keystroke |
| `.Blur()` | `wire:model.live.blur` (`.blur` before v4) | on leaving the field |

**Say `.Live()` on a control you read while the reader is still in the form**: a greeting that echoes a
name, a character count, a total, a select that fills a second select, a checkbox that shows a section, a
filter. A control you only read in the submit handler needs nothing.

- **An action** is anything that reaches your code: a press on a button or a link with a handler, a
  submit, Enter in a field, a key an `OnKeyDown` hears, another control's `.Live()` or `.Blur()` change, a
  navigation, an event a kit component raises into your code (a tab with `OnChange`, a menu item, a
  dragged row). Each is preceded by the
  values that waited, so a handler never reads a model that has not heard what was typed.
- **A kit control drawing itself is not an action.** Opening a `Ui.Select`, a `Ui.Dropdown`, a date or a
  time picker, moving its cursor with the arrows or the pointer, typing in its search field, paging a
  calendar: the page hears those only because it is what draws the
  control, and in Flux they are no request at all. Nothing that waits is written for them, so a rule of a
  field above the list does not speak when the list is opened. A **pick** in a bound control that waits is
  the same — the pick itself waits. What does count: a pick in a control that says `.Live()` or is held
  with `Value`/`OnChange`, a search field with an `OnInput` of yours, a `Ui.Dropdown.OnToggle`, a
  `Ui.Tabs.OnChange`, a "create" row — anything that reaches your code.
- **What the page does by itself is not an action.** A toast whose time is up closing itself, a chart
  reporting the size it was measured at, a list scrolling, a video playing on, an animation ending, an
  image arriving, the reply to a JavaScript call, a call your script makes into .NET: none of them writes
  a field the reader is still typing in, and none runs its rules. Their handlers run; what waits goes on
  waiting for the reader.
- **A render the server starts** — a timer, a push, a handler that finishes later — cannot ask the browser
  first. It sees the model as of the last action, and it does not take back what the reader has entered
  since: the text stays in the field, the box stays ticked, and both still travel with the next action.
- **Validation follows the value.** A field that waits shows no message while it is typed into or left;
  its rules run when its value arrives, and all of them on submit. `.Live()`, `.Debounce(…)` and `.Blur()`
  fields validate when they write.
- **A message goes as soon as the reader starts correcting it** — at the first key, and no rule runs for
  that. This is the one thing a waiting field sends while it is typed into: a message with no value in
  it, once, and only while a message shows.
- **The unsaved-changes guard** ([`ConfirmLeave`](#ask-before-leaving-unsaved-changes)) counts the first
  keystroke, sent or not.
- **A rule still running when Save is pressed is not saved past**: the submit runs every rule again and
  waits for the answers.
- **`AfterBind`** runs when the value is written — with the action for a control that waits.

`.Live()` is on every bound control of Core: `Input`, `Textarea`, `Select`. `.Debounce(…)` and `.Blur()`
are on `Input`, `Textarea`, `Ui.Input` and `Ui.Textarea`, for a field that is typed into — text, number,
date. A checkbox, a radio, a range and a colour are chosen rather than typed: with either step they
simply write as they are chosen. A file input's files travel with the submit, as in any form; `OnFiles`
is an event of its own and is not bound. One thing defeats a pause, by sending the value early: an
`OnKeyDown` or `OnKeyUp` on the field that hears every key. Give it
[`data-rask-keys`](https://rask.sh/docs/guides/js-interop-runtime/#keys-and-focus) and it hears only the keys it acts on.

The kit's `Ui.Input` and `Ui.Textarea` forward all three steps to Core's.

**Every chosen control of the kit follows the same rule**, and `.Live()` is on each of them: `Ui.Checkbox`,
`Ui.Switch`, `Ui.Select` (native, listbox, combobox, multiple), `Ui.Pillbox`, `Ui.Autocomplete`,
`Ui.CheckboxGroup`, `Ui.RadioGroup`, `Ui.Slider`, `Ui.Otp`, `Ui.Calendar`,
`Ui.DatePicker`, `Ui.TimePicker`, `Ui.Editor`, `Ui.Tabs`, `Ui.MenuCheckbox` and `Ui.MenuRadioGroup`.

```csharp
Ui.Select.Bind(() => m.Plan).Listbox[…]          // the list shows the pick; the model hears it with the next action
Ui.Select.Bind(() => m.Country).Live()[…]        // written on pick: AfterBind and a dependent select react at once
Ui.Checkbox.Bind(() => m.Agreed).Live()          // a section that shows while it is ticked
Ui.DatePicker.Bind(() => m.From).Live()          // a price that follows the date
```

- **A control of one element** — `Ui.Checkbox`, `Ui.Switch`, the native `Ui.Select` — waits in the browser,
  as an input does: ticking it sends nothing at all.
- **A control the kit draws in C#** — a listbox, a calendar, a group, a slider — still sends its own event,
  because the server draws the pick: the list shows the option, the calendar the day. What waits is the
  **write**: the model, the member's rules and `AfterBind` hear the value with the next action, in the
  order the controls were last touched, before the action's own handler. Nothing the page wrote runs per
  pick, and the page is not rendered again for one — only the control.
- **What counts as the next action here** is any handler that is not such a control drawing itself: a
  button of the page, a submit, another control's `.Live()` change, a controlled control's `OnChange`, and
  a callback the control itself raises into the page — `Ui.Otp`'s `OnComplete`, a tab's `OnClick`, a
  select's `OnInput` — which finds the model written.
- **Two controls that wait do not write each other.** A pick in the calendar leaves the listbox's pick
  waiting: a waiting control drawing itself is never an action, whichever control it is.
- **A listbox's pick is checked when it is written.** A bound `Ui.Select.Listbox` used to run its member's
  rules on submit only; they now run when the value arrives, like any bound control's.
- **Not preceded: a callback that captures no component.** The waiting picks are written ahead of a callback
  by way of the component that wrote it. A `static` lambda, or one that closes over locals only
  (`.OnComplete(_ => row.Done = true)` in a loop over rows), belongs to no component and runs without them;
  write it so that it touches the component (`_ => Finish(row)`).
- **A message under a control that waits** goes when the reader picks again, as it goes under a field at
  the first key. The pick marks the field modified at once.
- **A control a render removes takes its unwritten pick with it.** One that goes because the action itself
  replaces the page is written first. Following a link is not a handler of the page: it leaves the form,
  and a pick that was never written leaves with it — where a waiting input's text is still sent ahead of
  the navigation, because the browser holds it.
- A pick that waits lives in the session. A reconnect to a new session — a restarted server — forgets it,
  where a waiting input keeps what was typed in the browser.

A controlled input is not bound and none of this applies to it: `Input.Value(_filter).OnInput(v => _filter = v)`
hears every keystroke, as before.

### The two modes are exclusive

A control's value comes from exactly one place, and the step you open the chain with says which:

| Opened with | Mode | Its steps | Compile, but do nothing here |
| --- | --- | --- | --- |
| `.Bind(() => model.Field)` | bound | `Validate`, `AfterBind` | `Checked`, `OnInput`, `OnChange` |
| `.Value(v)` or `.Of<T>()` | controlled | `Checked`, `OnInput`, `OnChange` | `Validate`, `AfterBind` |

`Bind` and `Value` are the *openings* themselves, not steps you take later — taking one is what rules
the other out, so neither appears again on the chain. `Of<T>()` opens a controlled chain for a control
you are giving no value at all; if you want to supply one, that is `.Value(v)`.

Everything else — `Placeholder`, `Type`, `Required`, `Min`/`Max`, `OnFiles`, the whole `Class`/`Id`/
`Aria` element surface — belongs to neither and is reachable from both.

**Choosing a mode is enforced by the type**: the two openings live on the control's entry and nowhere
else, so taking one hands back the control and the other is not a member of it.

```csharp
Input.Bind(() => _model.Name).Value(_typed)   // ✗ no such step: Bind already answered this
```

The steps that FOLLOW an opening are not gated, and that is a deliberate trade. Each mode's steps used
to be declared only on that mode, which took a `Bound`/`Controlled` type argument threaded through every
generated signature to arrange — for a rule that only ever caught a property being set and then not
read. So this compiles now, and does nothing:

```csharp
Input.Bind(() => _model.Name).OnInput(v => _log = v)   // compiles; the handler never runs
```

The reason it does nothing is that bound mode already owns those: it derives the rendered value (and a
checkbox's `checked`) from the model and installs its own `oninput`/`onchange` write-back. Want a side
effect on
each bound write? That is what `AfterBind` is for.

The openings themselves stay exclusive: `Input.Bind(…)` and `Input.Value(…)` both live on the chain's
entry, so once you have taken one the other is not there to take.

Beyond the constraint/affordance attributes shared with plain HTML (`Min`/`Max`/`Step`/`Pattern`/
`MaxLength`/`MinLength`/`Multiple`/`Accept`/`List`/`Autocomplete`/`Autofocus`), the core `Input` also
carries the mobile & accessibility hints `InputMode` (on-screen keyboard), `EnterKeyHint` (action-key
label), `Spellcheck` (the enumerated `spellcheck="true|false"`), `Capture` (camera/mic for a file
input), and `DirName`. A control of your own forwards them the same way (see
[building-form-controls.md](https://rask.sh/docs/guides/building-form-controls/)).

> **Fractional numbers get `step="any"` automatically.** A `decimal`/`double`/`float`/`Half` binding
> renders `<input type="number" step="any">`. Without it HTML's default is `step="1"`, so the browser's own
> constraint validation rejects `42.50` and **refuses to fire submit** — silently, with nothing thrown and
> no validation message, which reads as the form being broken. Integral types keep the implicit whole-number
> constraint. An explicit `.Step(…)` always wins, and is worth setting for money (`.Step("0.01")` makes the
> spinner step by cents).

### File inputs

`InputType.File` turns an `<input>` into a file picker. Instead of binding a value, hand it an
`OnFiles` callback (a synchronous or an asynchronous handler) that receives the selected `IRaskFile`s (`Name`/`Size`/
`ContentType`/`OpenReadStream()`), and constrain the picker with `Accept`, `Multiple`, and `Capture`:

```csharp
Input.Of<string>().Type(InputType.File).Accept("image/*").Multiple()
     .OnFiles(async files => { foreach (var f in files) await Save(f); })
```

Uploading the bytes (streaming to a server endpoint, size limits, progress) is covered end-to-end in
[http-and-files.md](https://rask.sh/docs/guides/http-and-files/).

```csharp
Input.Bind(() => _model.Subscribe)   // bool     → checkbox
Input.Bind(() => _model.Age)         // int      → number
Input.Bind(() => _model.StartDate)   // DateOnly → date
Select.Bind(() => _model.Favorite)[Option.Value("Red")["Red"], Option.Value("Blue")["Blue"]]
Textarea.Bind(() => _model.Notes).Rows(3)
```

Those are the browser's own controls. An app normally writes the [UI kit](https://rask.sh/docs/guides/ui-kit/)'s, which bind the same
way: `Ui.DatePicker` for a date, `Ui.TimePicker` for a time (the two side by side for a date and a time),
`Ui.Select.Bind(…).Listbox[…]` for a choice and `Ui.Input` for text and numbers.



### Empty value handling

When the user clears an input, `BindingHelpers.TrySetTyped` decides what the empty string maps to:

| Property kind | Empty input becomes |
|---|---|
| `Nullable<T>` value type (`int?`, `DateOnly?`, …) | `null` |
| NRT-nullable reference type (`string?`) | `null` (detected via `NullabilityInfoContext`) |
| Non-nullable value type (`int`, `DateOnly`, enum) | `default(T)` — so a number/date/enum input is clearable |
| Non-nullable `string` | `""` (verbatim) |

A value that fails to parse (`"1.5"` or `"99999999999"` into an `int`, a date half typed) leaves the model
unchanged **and is not accepted in its place**: the field says what it could not read ("Enter a whole
number."), ahead of "required" and of any rule, and the submit handler does not run until a value arrives
that fits — see [what a number field says when it cannot read what was typed](https://rask.sh/docs/guides/forms-validation/#what-a-number-field-says-when-it-cannot-read-what-was-typed).

**Controls over a value type bind its nullable too.** `Ui.Checkbox` and `Ui.Switch` are over `bool`,
`Ui.Slider` over a number and `Ui.Calendar` over `DateOnly`, and each also binds the nullable
form: `Ui.Checkbox.Bind(() => model.InStock)` compiles whether `InStock` is a `bool` or a `bool?`, which is what a
[generated form model](https://rask.sh/docs/guides/data/#a-create-and-an-edit-form) holds. A `null` draws as the control's empty state
(unchecked, no day picked, a slider at its lowest value), and a change writes a value. `Value` and `OnChange` stay over the plain type. The
date controls go one further: `Ui.Calendar` binds a `DateOnly`, and behind Flux's mode steps a collection of days
(`Ui.Calendar.Multiple.Bind(…)`) or a `UiDateRange` (`Ui.Calendar.Range.Bind(…)`); `Ui.DatePicker` a `DateOnly` or,
as `Ui.DatePicker.Range`, a `UiDateRange`; and `Ui.TimePicker` a `TimeOnly?`, a `TimeOnly` or a collection of
times ([UI kit](https://rask.sh/docs/guides/ui-kit/#calendar)).

Every BCL [`IParsable<T>`](https://learn.microsoft.com/dotnet/api/system.iparsable-1) type (numbers,
`Guid`, `DateOnly`/`DateTime`/`TimeOnly`, `bool`, …) binds with no setup, and so does a **custom**
`IParsable<T>` value type under the default interpreter build. For a full [WASM AOT](https://rask.sh/docs/guides/aot/) build,
register your custom form-field types once at startup with `RaskBinding.RegisterParsable<Money>()` —
custom route/query param types are registered automatically by the generator.



### Binding lifecycle

Each change handler runs in order: write the value, `NotifyFieldChanged`, run `AfterBind`
(if supplied, only when a write actually happened), `NotifyFieldTouched`, then re-validate the field.
When that happens is the control's [bind timing](#bind-timing): with the next action unless the chain
says `.Live()`, `.Debounce(…)` or `.Blur()`. A text field sent at every keystroke
(`.Debounce(TimeSpan.Zero)`) stays quiet until it is touched, then re-validates on every keystroke.

`AfterBind` fires **after** the new value is written and before validators run — handy for dependent
fields (pick a country, repopulate the city dropdown in the same render):

```csharp
Select.Bind(() => _model.Country)
    .AfterBind(c => { _cities = Cities[c]; _model.City = _cities[0]; })[ /* options */ ]
Select.Bind(() => _model.City)[_cities.Select(c => Option.Value(c)[c])]
```


It takes an asynchronous handler just as readily — one property, either shape — and awaits before the
post-handler render, so a dependent async lookup (repopulate a dropdown from an API) surfaces its
loading state on its own, with no manual `StateHasChanged()`:


### What a bind costs, and the one case worth changing

`Bind(() => …)` takes an `Expression<Func<T>>`, and **the C# compiler builds that tree at the call
site on every render**. Building it resolves a member token on the bound property's *declaring type*,
and that cost scales with how many members the type has:

| what you bind | B/render |
| --- | --- |
| `Input.Value(…)` — controlled, no bind at all | 1216 |
| `Input.Bind(() => Model.Name)` — a plain model | 3041 |
| `Input.Bind(() => Draft)` — a property on the **component** | 5011 |

The third row is the surprise, and it is invisible at the call site: the two spellings look equally
cheap. A component is a markup host, so the generator injects the chain entries into it — around 430
members — and binding a property *declared there* pays for resolving a token on a type that large.
Binding the same value on a plain model does not.

**For nearly every app this does not matter.** A form re-renders on interaction, not in a loop; a few
kilobytes per render is well under the noise. Reach for the fix below only when a bound control is in
something genuinely hot — a virtualized grid, a control that re-renders on every keystroke of a large
document.

**The fix is at the call site: hoist the expression into a field**, so it is built once instead of
per render.

```csharp
using System.Linq.Expressions;

public sealed partial class Editor : Component
{
    private readonly Model _model = new();

    // Built once, in the constructor, rather than on every render.
    private readonly Expression<Func<string>> _name;

    public Editor() => _name = () => _model.Name;

    protected override Component Render() => Input.Bind(_name);
}
```

The two rows converge once the tree stops being rebuilt: **2721 B/render** hoisted against a plain
model, **2753 B** hoisted against a property on the component — so hoisting does not merely help the
expensive shape, it erases the difference. What remains is the binding machinery (`FieldIdentifier`,
validator registration, owner tracking), which is shared with the plain-model case.

Every number on this page is pinned by `BuilderEntryAllocationPinTests`, so none of them can drift
without a test going red.

The other move is simply **to bind a plain model rather than a property on the component**
(`() => _model.Name`, not `() => Draft`), which is what most code does already and costs 3041 B rather
than 5011 B without any ceremony.

> Why not fix this in the framework? Measured and ruled out in
> [#803](https://github.com/pal-tamas/rask/issues/803): a Roslyn interceptor cannot help — an
> interceptor must keep the intercepted method's signature (CS9144), and the lambda is converted to an
> expression tree when the call is *bound*, before interception applies. Adding a `Func<T>` overload
> does not help either, because C# prefers the `Expression` one. What remains is making the injected
> chain entries inheritable instead, which is a change to how every markup host is generated.

---

## 2. `Form.Model(…)` and the `EditContext`

`Form.Model(model)` wraps the inputs and owns an `EditContext` — the per-field state store plus the
validator pipeline. Bound inputs inside the form discover that context automatically.

```csharp
Form.Model(_model).OnSubmit(m => Console.WriteLine(m.Username))[
    Input.Bind(() => _model.Username),
    Button.Type(ButtonType.Submit)["Sign up"]
]
```

A field's rule is a step on its input — the value in, the messages that reject it out:

```csharp
Input.Bind(() => _model.Username)
    .Validate(v => v.Length >= 3 ? [] : ["At least 3 characters."])
```

A rule across fields goes on the form the same way, and either can be `async`: see
[validation](https://rask.sh/docs/guides/forms-validation/).

Submit runs every rule, marks every registered field touched, then routes:

- valid → `OnSubmit` (or, if unset, `OnAnySubmit` with the raw `FormData`),
- invalid → `OnInvalidSubmit`.

`OnSubmit` / `OnInvalidSubmit` are one `Callback<TModel>` each, so either takes a synchronous
(`Action<TModel>`) or an asynchronous (`Func<TModel, Task>`) handler — a bare lambda or a method group,
no cast, no `…Async` twin.

While a submit is in flight the form takes no second one, as Livewire's `wire:submit` does: at the press,
before anything is sent, the runtime disables the form's submit buttons (a `<button form="…">` outside it
too) and makes its text fields `readonly`, and it gives them back when the handler and the render it caused
are done — on the Server host with that submit's ack, in a WebAssembly app when the dispatch returns. A
double click, a second Enter or a script's `requestSubmit()` meanwhile is dropped in the browser and never
sent, so a non-repeatable save (an upload, a payment) runs once. The fields stay in what is sent:
`readonly`, unlike `disabled`, keeps a value in the form data. A render meanwhile does not hand the
controls back early, and what it says of them (`.Disabled(submitting)` below) is what they are left with —
except a field it shows a message under: a submit whose rule awaits a lookup draws what it already knows at
that await, and the field it calls wrong is handed back then, so the first key of the correction is heard.
The button's spinner still waits its 200 ms, so a fast save never flashes one. A plain button's `OnClick`
is not held: Livewire disables nothing for `wire:click`.

### Children that follow the submit

Children are normally a fixed list. Give the form a **function** instead and it is called on every
render with whether a submit is in flight, so the markup can say so without the page tracking it:

```csharp
Form.Model(_model).OnSubmit(Save)[submitting => [
    Input.Bind(() => _model.Username).Disabled(submitting),
    Button.Type(ButtonType.Submit).Disabled(submitting)[submitting ? "Saving…" : "Sign up"]
]]
```

The flag is true from the moment the submit handler starts until it returns — including when it
throws — and the form re-renders on both edges. Only an `async` handler can be observed in that
state: a synchronous one returns before there is a frame to paint.


A handler that throws tells the reader by itself: an error toast, *The save failed. Please try again.*,
in the reader's language (`RaskString.FormSaveFailed`). Their values stay in the fields and the submit is
free for another try. A form that draws its own messages with `Validation.Summary` says the sentence
there and raises no toast. The exception's own text is never shown — a server app in Development adds
its type and message — and it is logged as an error. The same value's `Error` holds it
(`f => [ f.Error is null ? null : … ]`), for a page that wants to draw more than the toast.

To handle it yourself, catch it in the handler. What the handler catches never reaches the form, so
there is no toast. A handler outside a form that throws gets a sentence of its own
([a handler that fails](https://rask.sh/docs/guides/composition-lists/#a-handler-that-fails)):

```csharp
async Task Save(SignUp model)
{
    try { await Accounts.Create(model); }
    catch (MailServerDownException) { Toast.Warning("Signed up. The welcome mail will follow."); }
}
```

A handler that names the fields it is about — the store refusing a duplicate — is shown
[under those fields](https://rask.sh/docs/guides/forms-validation/#the-database-said-no) and is not an error at all; a
DataAnnotations `ValidationException` is treated the same way. A refusal that names no field shows its
own sentence — in `Validation.Summary`, or as the error toast when the form has none — in place of
*The save failed*. A cancelled save says nothing. See
[a save that fails](https://rask.sh/docs/guides/forms-validation/#a-save-that-fails).

The fixed-list forms are untouched and still bind exactly as before:

```csharp
Form.Model(_model)[Input.Bind(() => _model.Username), Button.Type(ButtonType.Submit)["Sign up"]]
```

Only a form offers the function form. It is an indexer declared on `Form` itself, so
`Div[submitting => …]` does not compile — there is no submit state behind a `<div>` to report. See
[`Form`](https://github.com/pal-tamas/rask/blob/main/src/Rask.Core/Dom/HTMLFormElement.cs).

### Asking before an action

`.Confirm("…")` asks the reader before a click or a submit is sent — in the browser's own `confirm` dialog, or
in [`Ui.Confirm`](#ask-for-a-confirmation-in-a-dialog-of-your-own) when the layout has one. Livewire's
`wire:confirm`. It is a step on every element, so it works on a plain `Button`, a kit `Ui.Button`, a `Form` and
a submit button alike:

```csharp
Ui.Button.Icon(Ui.IconName.Trash).OnClick(() => Delete(row)).Confirm("Delete the partner?")

// Livewire's wire:confirm.prompt: the reader types the name, and only an exact match goes (case and all).
Ui.Button.Danger.OnClick(() => Delete(row)).Confirm("Type the partner's name to delete it", expect: row.Name)

// On a form it asks before the submit; on a submit button, before the submit that button makes.
Form.Model(_order).OnSubmit(Send).Confirm("Send the order?")[ … ]
```


The question is asked in the browser, before anything leaves it, so it costs no round trip. Cancelled, **nothing
is sent**: not the click or the submit, and not the fields still waiting to go with it — a bound field that
waits for the next action keeps waiting. Both hosts ask the same way. The element carries the question as
`data-rask-confirm` (and the expected answer as `data-rask-confirm-expect`), so it is in the page's own words
and in its language. Do not put a secret in `expect:` — it is in the page.

#### Ask for a confirmation in a dialog of your own

The browser's `confirm` and `prompt` cannot be styled or translated. Put `Ui.Confirm` in the layout, once, and
every `.Confirm("…")` under it asks in that dialog instead — a [`Ui.Modal`](https://rask.sh/docs/guides/ui-kit/#who-owns-the-state) drawn
as Flux's confirmation: a short heading, the call site's question as the sentence beneath it, No and Yes.

```csharp
Ui.Main[Outlet],
Ui.Toast,
Ui.ConfirmLeave,
Ui.Confirm.Heading("Megerősítés").No("Nem").Yes("Igen")   // "Confirm", "No" and "Yes" when unset
```

The call sites do not change. No, the close button, Escape and a press outside call the action off; only Yes
lets it go — the same press, or the same submit by the same button, and a form is held only then. With
`expect:` the dialog shows a field, and Yes waits until the answer is typed exactly, case and all; Enter in the
field is Yes once it matches. It all happens in the browser: opening the dialog, typing in it and closing it
send the server nothing, and focus goes back to the control that was pressed. A question can bring its own title,
which replaces the layout's for that question alone:

```csharp
Ui.Button.Danger.OnClick(() => Delete(row)).Confirm("Biztosan törli a céget?", heading: "Cég törlése")["Törlés"]
```

Without `Ui.Confirm` the browser asks, as before, and `heading:` is not shown. The words default from the [string catalog](https://rask.sh/docs/guides/localization/) (`ConfirmHeading`, `ConfirmNo`,
`ConfirmYes`).

### Ask before leaving unsaved changes

`ConfirmLeave` makes a form ask before the reader walks away from what they typed:

```csharp
Form.Model(_model).OnSubmit(Save)
    .ConfirmLeave("Leave without saving your changes?")[
    Input.Bind(() => _model.Name),
    Button.Type(ButtonType.Submit)["Save"]
]
```


The form counts as **unsaved from the first thing typed or changed in it**. That is decided in the
browser, so it holds for a value that has not reached the server yet — and it is not a comparison with
the original values: typing a value and typing it back still counts. While it is unsaved, every way out
of the page asks first:

| The reader… | What asks |
| --- | --- |
| follows a link inside the app (`NavLink`, a Markdown link to another page) | your message, in the browser's own `confirm` dialog — or in [`Ui.ConfirmLeave`](#ask-in-a-dialog-of-your-own) when the layout has one |
| is navigated by front-end code (`__raskHost.navigate(…)`, an island's router) | the same dialog |
| presses Back or Forward | the same dialog; staying puts the address and the history back where they were |
| closes the tab, reloads, or follows a link out of the app | the browser's own "leave site?" prompt. No page can set its text, so your message is not shown there. |

Staying sends nothing to the server and leaves the page exactly as it was, typed values included.

The form is **clean again after a submit that passed validation and whose handler returned** — and
unsaved again at the next edit. A submit validation refuses, or a handler that throws, leaves it
unsaved. A navigation your own handler makes never asks, so the usual "save, then go to the list" needs
nothing extra:

```csharp
private async Task Save(Product product)
{
    await product.Save();
    Routes.ProductsPage().Go();   // not asked about: the app is leaving, not the reader
}
```

#### Ask in a dialog of your own

Put `Ui.ConfirmLeave` in the layout, once, and every guarded form under it asks in that dialog instead of the
browser's `confirm` — a [`Ui.Modal`](https://rask.sh/docs/guides/ui-kit/#who-owns-the-state) drawn as `Ui.Confirm` is, with the form's
message as its heading:

```csharp
Ui.Main[Outlet],
Ui.Toast,
Ui.ConfirmLeave.Stay("Nem").Leave("Igen")   // the two buttons; "Stay" and "Leave" when unset
Ui.ConfirmLeave.Heading("Unsaved changes")  // a short title over the form's question, as Flux titles a confirmation
```

The forms do not change. `Stay`, the close button, Escape and a press outside the dialog all keep the reader
on the page, as it was; `Leave` carries on with the navigation that was asked for — the same link, the same
`navigate(…)`, the same history entry Back was going to. The dialog is rendered closed and opened in the
browser, so it needs no round trip to appear. Closing the tab, a reload and a link out of the app still get
the browser's own prompt: no page can draw that one.

A form that is no longer on the page guards nothing. With several guarded forms on one page, the first
unsaved one asks, with its own message.

Three things to know:

- **What counts as an edit** is the browser's `input` and `change` events inside the form, and every value
  the reader chooses in a kit control that draws itself — a listbox, a combobox or a pillbox option, a pill
  taken away, a day in a `Ui.Calendar` or a `Ui.DatePicker`, a time, a colour, a rating, a picker's clear
  button, a `Ui.FileItemRemove`. The browser raises no event for those, so the page that hears the press
  says so on the form, bound with `Bind` or held with `Value`/`OnChange` alike, written to the model yet or
  not. Opening a list, moving through a calendar and typing in a list's search field change nothing and
  count for nothing. A value the page writes by itself is no edit, and neither is a button of your own
  whose `OnClick` sets a property.
- **A pick is counted once the page has heard it**, which is a round trip after the press. A reader who
  picks and follows a link within that instant is not asked.
- **Which way Back went** is read from the Navigation API. On a browser without it (Safari before 26.2,
  Firefox before 147) a refused Back or Forward still leaves the reader on the page with the right
  address, but as a new history entry — and Back and Forward ask with `confirm` there even when the
  layout has a `Ui.ConfirmLeave`, because the move cannot be made a second time.
- **A press in the first instant** — before the script that guards the form has arrived — is not asked
  about. The guard is one of the [behaviour hooks](https://rask.sh/docs/guides/js-interop-runtime/#how-the-hooks-load): a page
  rendered with the form loads it with the runtime, so this is a matter of milliseconds.

### Auto-created vs explicit `Context`

By default the form creates (and caches per model reference) its own `EditContext` — it persists
across renders of the same model, so field state survives re-renders. Pass `Context:` to own the
instance yourself when you need to drive validation imperatively, register an
`IAsyncFieldValidator`, or tune `ValidatingStickyMs`:

```csharp
_ctx = new EditContext(_model);
_ctx.AddValidator(new SlowTitleValidator());

Form.Model(_model).OnSubmit(m => _submission = "Saved").Context(_ctx)[
    Input.Bind(() => _model.Title),
    Button.Type(ButtonType.Button).OnClick(() => _ctx.Validate().AsTask())["Validate now"],
    Button.Type(ButtonType.Submit).Disabled(_ctx.IsValidatingAny)["Save"]
]
```

`Form` requires either `Model` or `Context` — they are the two ways to open its chain.

### Binding a data model

A [`Rask.Data`](https://rask.sh/docs/guides/data/) entity gets a form shape generated beside it: `ProductModel` beside `Product`, every
mapped property settable and the entity's DataAnnotations copied onto it. Bind one and the form validates by
the entity's own rules, with nothing declared here:

```csharp
private readonly ProductModel _product = new();

Form.Model(_product).OnSubmit(Create)[
    Input.Bind(() => _product.Name),
    Input.Bind(() => _product.Price),
    Button.Type(ButtonType.Submit)["Create"]
]

private async Task Create(ProductModel product)
{
    await using var db = await contexts.CreateDbContextAsync(CancellationToken);
    db.Add(Product.Create(product.Name, product.Price));
    await db.SaveChangesAsync(CancellationToken);
}
```

The model is what the writes on the type take — `Product.Create(model)`, `Product.Update(id, model)` —
or what a command carries to a handler. It carries no id, so the row an edit saves is the one the page's own route
names, and it carries the row's `Version` back, so `Update` throws `DbUpdateConcurrencyException` when it
lost a race rather than overwriting the other one. See
[a create and an edit form](https://rask.sh/docs/guides/data/#a-create-and-an-edit-form).

### An edit page that reads its record

The **page** reads the record, in `OnUpdated` (it runs again when the route's id changes) or in `OnMount`, and
draws the form only once the record is there:

```csharp
[Route("/trailers/{Id}")]
public sealed partial class TrailerEditPage : Component
{
    private TrailerModel? _trailer;

    [RouteParam] public int Id { get; set; }

    protected override async Task OnUpdated() => _trailer = await Trailer.Model(Id);

    protected override Component? Render() =>
        _trailer is null
            ? Ui.Callout["No such trailer"]
            : TrailerForm.Key(Id).Model(_trailer);
}
```

No "read yet?" flag is needed beside the `null` check. The first `GET` and a navigation in place both
[wait for the read](https://rask.sh/docs/guides/routing/#the-page-is-sent-when-its-first-reads-are-back), so a read that is back within
the budget is sent as **one** frame — the form with its record — and "No such trailer" is only ever seen for a
trailer that does not exist. A read that outlasts the budget is painted as the `null` branch first and as the
form when it lands; the page is drawn again when its hook's `await` completes, with nothing to call.

A read that **throws** shows the [built-in error page](https://rask.sh/docs/guides/lifecycle/#gotcha-a-faulted-async-hook-takes-the-page-not-the-component)
in place of the page (the first `GET` answers 500). To say a sentence of your own instead, wrap the read in
`try`/`catch` and draw it from `Render`.

A form is its **model's** form: `Form.Model(model)` keeps one `EditContext` per model instance, so handing it
another instance — the next record under the same `Key`, a record read again — is a new form with that
instance's values, and nothing typed into the previous one is left in its fields.

**A form component reads its model where it draws the form.** A component of your own that wraps the form takes
the model as a prop and uses it in `Render`:

```csharp
public sealed partial class TrailerForm : Component
{
    public TrailerModel? Model { get; set; }

    protected override Component? Render() =>
        Model is null ? null : Form.Model(Model).OnSubmit(Save)[Ui.Input.Bind(() => Model.Name).Label("Name")];
}
```

The trap is a copy: `_model = Model;` in the component's `OnMount` is taken **once**, when the component is
mounted. If the page mounted it before its read was back — `TrailerForm.Model(_trailer)` with no
`null` check around it — the copy is `null` and stays `null` however many times the page hands it the record
afterwards. Read the prop in `Render`; where a copy is really wanted (a draft the reader may discard), take it
in `OnUpdated`, which runs again whenever the prop changes, or mount the component only once the model is read,
as the page above does.

### Rendering messages

Two headless components read the context — both take a required `Template` step, written first, so you own the markup,
and both render nothing when there's nothing to show:

```csharp
Validation.Message.Template(errs => Div.Class("field-error")[errs[0]]).For(() => _model.Email)

Validation.Summary.Template(entries => Ul[entries.Select(e => Li[Strong[e.Field], ": ", e.Message])])
```

`Validation.Message.For` keys a single field; `Validation.Summary` lists every `ValidationEntry`
(`Field` + `Message`), with form-level messages carrying an empty `Field`.


### Controls at a glance

Every input works in two shapes — **controlled** (`Value` + `OnChange`, the parent owns the value) and
**bound** (`.Bind(() => model.X)`, two-way). A readout rendered *outside* the control follows a
controlled one at every change, and a bound one that says `.Live()`. The matrix below covers text, textarea and select; the [UI kit](https://rask.sh/docs/guides/ui-kit/)'s controls
take the same two shapes, since they implement the same `IFormControl<T>`.




**Labels.** `Ui.Input`, `Ui.Textarea` and `Ui.Select` are Flux UI's: `Label` draws a label over the field,
`Description` help text under the label, and each bound field shows its own validation message under the control:


### Accessible validation

A control of your own (see [building form controls](https://rask.sh/docs/guides/building-form-controls/)),
and the [UI kit](https://rask.sh/docs/guides/ui-kit/)'s controls) expose validation to assistive tech automatically — no extra props.
When a bound field has messages, the control renders `aria-invalid="true"` and an `aria-describedby` that
points at the error message's `id` (and the description's `id` when `.Description(…)` is set), so a screen
reader reads the error with the field rather than detached from it. Valid fields with a `Description` still get
`aria-describedby` to it.

**Composing the field yourself.** The kit's [`Ui.Field`](https://rask.sh/docs/guides/ui-kit/#fields-label-description-error) is the
same three parts as components — Flux UI's field — for when the label, the help text or the message needs
to sit somewhere a control's own `Label` would not put it:

```csharp
Ui.Field[
    Ui.Label.Badge("Required")["Email"],
    Ui.Input.Bind(() => _model.Email).ShowValidation(false),
    Ui.Error,                                   // or Ui.Error.For(() => _model.Email) outside a field
    Ui.Description["We only write about your order."]
]
```

`Ui.Label` is a `<label for>` pointing at the control, and `Ui.Error` is a `role="alert"` live region that
is always in the page and shows the first message the form holds for the bound member.
`Ui.Fieldset.Legend("Shipping address")[…]` groups several fields under one heading.

A combobox control — [`Ui.Select`](https://rask.sh/docs/guides/ui-kit/#select) with `.Listbox` or `.Combobox`, over one answer or many —
carries `role="combobox"` on a real `<button>` or text `<input>`. Both are labelable elements, so the
select's `Label` names it through the field's `<label for>`, as it does the native `<select>`. Alongside
it goes the popup contract: `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls` naming the
list, and `aria-activedescendant` naming the option the keyboard cursor is on while focus stays on the
box. Options are `role="option"` carrying `aria-selected`, and an unavailable one carries
`aria-disabled` — **present only when it is true**, since a valueless `aria-disabled` reads as `true`
and would mark every option unavailable.

A list that takes more than one answer says so on the listbox itself, with
`aria-multiselectable="true"`. Without it a reader meets options that each carry `aria-selected`
and has no way to learn that a second one is allowed — the options look identical either way, so
the fact that several may be chosen lives nowhere else.

If you build your own control from the core `Input`/`Validation.Message` primitives (§9), mirror the same
three attributes so the field stays accessible: `aria-invalid` on the control, `aria-describedby` from
the control to the message `id`, and `role="alert"` on the message container. See
[accessibility.md](https://rask.sh/docs/guides/accessibility/#form-validation).
