reactor-forms
Skill839 starsChanged 6 days agoMITView on GitHub
Reactor forms and validation — `UseValidationContext`, built-in validators (`Validate.Required`, `Validate.Email`, `Validate.MinLength`, etc.), `FormField` helper, masked input via `MaskEngine`, `InputFormatter`. Use when building data-entry screens, validation flows, or controlled-input forms.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- analyzer-dymSkill
- coverage-upliftSkill
- perf-compareSkill
- pr-lifecycleSkill
- pr-reviewSkill
- reactor-advancedSkill
- reactor-asyncSkill
- reactor-build-and-checkSkill
- reactor-chartsSkill
- reactor-commandingSkill
- reactor-designSkill
- reactor-devtoolsSkill
- reactor-dockingSkill
- reactor-dslSkill
- reactor-getting-startedSkill
- reactor-inputSkill
- reactor-navigationSkill
- reactor-recipesSkill
- reactor-windowingSkill
- reactor-wrappingSkill
- reactor-appSkill
- ChartingSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 33
Every input takes (value, setter).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Forms and Validation in Reactor
Reactor forms use a controlled-input pattern — every input has an
explicit (value, setter) pair driven by UseState. There is no two-way
binding. Validation is layered on top via UseValidationContext and
declarative .Validate() modifiers.
Controlled prop note: factory call sites keep the plain
(value, setter)shape, but the underlying element records useOptional<T>. Read element props with.Value/.GetValueOrDefault(...); useOptional<T>.Unsetonly when the WinUI control should own the value. Seemigration/050-optional-t.md.
Quick reference
| API | Purpose |
TextBox(value, setValue) | Controlled text input |
UseValidationContext() | Track validation messages, touched/dirty state |
.Validate(...) | Attach built-in validators to an input |
FormField(input, label: ...) | Wraps input with label, error display, required marker |
new MaskEngine(...) | Masked text input (phone, SSN, etc.) |
InputFormatter.Currency(...) | Format-as-you-type |
1. Controlled inputs
Every input takes (value, setter). State lives in the component:
var (name, setName) = UseState("");
var (age, setAge) = UseState(0);
var (agreed, setAgreed) = UseState(false);
return VStack(12,
TextBox(name, setName, placeholderText: "Name"),
NumberBox(age, setAge),
CheckBox(agreed, setAgreed, label: "I agree"),
Button("Submit", onSubmit).IsEnabled(!(string.IsNullOrEmpty(name) || !agreed))
);Available input types
| Factory | Value type | Common modifiers |
TextBox(value, setValue, placeholderText, header) | string | .Header(), .IsReadOnly(), .AcceptsReturn(), .TextWrapping(), .MaxLength(n), .NumericInput(), .EmailInput(), .Changed(handler) |
PasswordBox(password, setPassword, placeholderText) | string | .Header(text), .MaxLength(n), .PasswordChanged(handler) |
NumberBox(value, setValue, header) | double | .PlaceholderText(text), .Range(min, max), .SpinButtons(...) |
Slider(value, min, max, setValue) | double | .Header(), .StepFrequency() |
ToggleSwitch(isOn, setIsOn, header, onContent, offContent) | bool | .Header() |
CheckBox(isChecked, setIsChecked, label) | bool | — |
RadioButtons(items, selected, setSelected, header) | int | .Set(rb => ...) |
ComboBox(items, selected, setSelected) | object | .Header(), .IsEditable(), .PlaceholderText(text) |
DatePicker(date, setDate, header) | DateTimeOffset | .Set(dp => ...) |
TimePicker(time, setTime, header) | TimeSpan | .Set(tp => ...) |
AutoSuggestBox(text, setText, onQuerySubmitted) | string | .PlaceholderText(text), .Set(asb => ...) |
RichEditBox(doc, setDoc, header) | string | .PlaceholderText(text), .Set(reb => ...) |
CalendarDatePicker(date, setDate) | DateTimeOffset? | .PlaceholderText(text), .Set(cdp => ...) |
Named-input shapes (Spec 039 §17.3): .NumericInput() / .EmailInput()
preconfigure InputScope and IME hints so on-screen / soft keyboards open
in the right mode. Stack them with validators:
TextBox(email, setEmail, placeholderText: "you@example.com")
.EmailInput()
.MaxLength(254)
.Validate("email", email, Validate.Required(), Validate.Email())For modifiers that aren't in the typed surface (e.g. PasswordRevealMode,
date min/max), use .Set(native => ...) to reach the underlying WinUI control.
The full catalog is in references/reactor.api.txt.
2. Simple validation (derived booleans)
For trivial forms, derive validation from state:
var (email, setEmail) = UseState("");
var isValid = email.Contains('@') && email.Length > 3;
return VStack(12,
TextBox(email, setEmail, placeholderText: "Email"),
Button("Submit", onSubmit).IsEnabled(isValid)
);This is fine for 1–2 fields. For anything more, use UseValidationContext.
3. UseValidationContext
Tracks per-field validation messages, touched/dirty state, and overall form validity:
var validation = this.UseValidationContext();
var (name, setName) = UseState("");
var (email, setEmail) = UseState("");
return VStack(12,
TextBox(name, setName, placeholderText: "Name")
.Validate("name", name,
Validate.Required("Name is required"),
Validate.MinLength(2, "Name too short")),
TextBox(email, setEmail, placeholderText: "Email")
.Validate("email", email,
Validate.Required("Email is required"),
Validate.Email("Invalid email")),
Button("Submit", () =>
{
validation.MarkAllTouched();
if (validation.IsValid())
Submit(name, email);
})
);.Validate(fieldName, value, ...) resolves the surrounding ValidationContext
through React-style ambient context — you do not pass validation explicitly.
Passing the current value (the second arg) runs the validators right there,
during the render, so a When(validation.HasError("email"), ...) placed after
the field reads the result in the same pass rather than one render late. The
validator-only overload .Validate(fieldName, ...) has no value to check, so it
only records the validators: nothing runs them, and on a bare control nothing
registers the field either. Inside a FormField the field is registered on
mount, so MarkAllTouched() covers it.
You do not need .Provide(ValidationContexts.Current, validation): a
component-local context is published to the rendered subtree automatically, so
FormField, ValidationVisualizer, and nested components find it. Providing one
explicitly is what descendants resolve, but it does not redirect the providing
component's own .Validate() calls — those already ran while the tree was being
built. To pool several components' fields into one context, provide it from a
parent and let each child call UseValidationContext().
Mutating the context re-renders the component that created it. That is why
MarkAllTouched() alone reveals the errors on a failed submit, even though no
component state changed.
ValidationContext API
| Member | Purpose |
.IsValid() | true when no field has Error-severity messages |
.IsDirty() | true when any registered field differs from initial value |
.IsDirty("field") | Per-field dirty check |
.MarkAllTouched() | Mark every registered field touched (typical on submit) |
.MarkTouched("field") | Mark a single field touched |
.Reset("field") | Reset one field to initial value, returns the initial |
.ResetAll() | Reset all fields to initial values |
.ClearAll() | Clear all messages (preserve touched/initial state) |
.GetMessages("field") | Get error messages for a specific field |
.IsTouched("field") | Whether the user has interacted with a field |
.Changed | Event raised when the context's observable state changes |
4. Built-in validators
The .Validate() modifier accepts an array of validators:
| Validator | Purpose |
Validate.Required(msg) | Non-empty |
Validate.MinLength(n, msg) | Minimum string length |
Validate.MaxLength(n, msg) | Maximum string length |
Validate.Email(msg) | Email format |
Validate.Match(pattern, msg) | Custom regex pattern |
Validate.Range(min, max, msg) | Numeric range |
Validate.Must<T>(predicate, msg) | Arbitrary predicate |
Validate.EqualTo<T>(value, msg) | Fields must match (confirm password) |
Validate.Url(msg) | URL format |
Validate.MustBeTrue(msg) | Boolean must be true (checkboxes) |
5. FormField helper
FormField wraps an input with a label, required marker, description
text, and error display:
var validation = this.UseValidationContext();
var (name, setName) = UseState("");
return FormField(
TextBox(name, setName, placeholderText: "Enter your name")
.Validate("name", name, Validate.Required("Required")),
label: "Full Name",
required: true,
description: "As it appears on your ID",
showWhen: ShowWhen.WhenTouched // or Always, WhenDirty, AfterFirstSubmit, Never
);ShowWhen controls when error messages appear:
WhenTouched— after the user has interacted with the field (recommended default).FormFieldmarks its own field touched on blur; elsewhere callMarkTouched.Always— immediately, even before user interactionWhenDirty— only after the value has changed from its baseline. RequiresSetInitialValue(field, value): with no baseline recorded a field is never dirty, so this policy stays silent forever.AfterFirstSubmit— only after the first submit attempt, which isMarkAllTouched().ResetAll()clears it, so the next reveal waits for a fresh submit.
The verdict itself is unaffected by any of these — IsValid() and
GetMessages() are current from the first render. ShowWhen only gates display.
6. Masked input
MaskEngine restricts and formats input as the user types:
var mask = UseMemo(() => new MaskEngine(MaskPreset.PhoneUS));
var (phone, setPhone) = UseState("");
return TextBox(phone, v => setPhone(mask.Apply(v)),
placeholderText: "(555) 555-0123");Mask presets
| Preset | Format |
MaskPreset.PhoneUS | (___) ___-____ |
MaskPreset.SSN | ___-__-____ |
MaskPreset.ZipCode | _____ |
MaskPreset.ZipCodePlus4 | _____-____ |
MaskPreset.CreditCard | ____ ____ ____ ____ |
MaskPreset.Date | __/__/____ |
Custom masks: new MaskEngine("AA-####") where A = letter,
# = digit, * = any.
7. Input formatters
InputFormatter applies format-as-you-type transformations:
var (amount, setAmount) = UseState("");
return TextBox(amount,
v => setAmount(InputFormatter.Currency(symbol: "$").Format(v)),
placeholderText: "$0.00");| Formatter | Effect |
InputFormatter.Currency(symbol: "$") | $1,234.56 |
InputFormatter.PhoneUS | (555) 555-0123 |
InputFormatter.UpperCase | Force uppercase |
InputFormatter.LowerCase | Force lowercase |
InputFormatter.TitleCase | Title Case |
InputFormatter.MaxLength(n) | Truncate at n chars |
InputFormatter.AllowOnly(regex) | Whitelist characters |
Critical gotchas
- Always use controlled inputs —
(value, setter)pair. There is no uncontrolled / two-way binding in Reactor. - Call
validation.MarkAllTouched()before submit — when fields use the.Validate(name, value, ...)form, validators run automatically every render, but errors stay hidden until each field is touched. Mark all registered fields touched on submit so error messages reveal at once, then gate onvalidation.IsValid(). - Use
ShowWhen.WhenTouched(default) — showing errors immediately on page load is hostile UX. - MaskEngine and InputFormatter are different — masks restrict what characters can be entered; formatters transform the display.
- Don't mix simple validation and UseValidationContext — pick one approach per form.
- FormField handles layout and error display — don't manually build error message TextBlocks when using FormField.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

