| name | reactor-forms |
|---|---|
| description | Forms, validation, and controlled inputs in Reactor — UseValidationContext, built-in validators, FormField helper, masked input, input formatters. Load this when building form UIs, validation flows, or data-entry screens. |
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.
| API | Purpose |
|---|---|
TextBox(value, setValue) |
Controlled text input |
UseValidationContext() |
Track validation messages, touched/dirty state |
.Validate(...) |
Attach built-in validators to an input |
FormField(label, input) |
Wraps input with label, error display, required marker |
new MaskEngine(...) |
Masked text input (phone, SSN, etc.) |
InputFormatter.Currency(...) |
Format-as-you-type |
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("I agree", agreed, setAgreed),
Button("Submit", onSubmit).IsEnabled(!(string.IsNullOrEmpty(name) || !agreed))
);| Factory | Value type | Common modifiers |
|---|---|---|
TextBox(text, setText) |
string |
.PlaceholderText(), .MaxLength(n), .NumericInput(), .EmailInput(), .Changed(handler) |
PasswordBox(text, setText) |
string |
.PlaceholderText(), .MaxLength(n), .PasswordRevealMode(), .PasswordChanged(handler) |
NumberBox(value, setValue) |
double |
.Min(), .Max(), .SmallChange() |
Slider(value, min, max, setValue) |
double |
.StepFrequency() |
ToggleSwitch(isOn, setIsOn) |
bool |
.OnContent(), .OffContent() |
CheckBox(label, isChecked, setIsChecked) |
bool |
— |
RadioButtons(items, selected, setSelected) |
int |
.Header() |
ComboBox(items, selected, setSelected) |
object |
.PlaceholderText() |
DatePicker(date, setDate) |
DateTimeOffset |
.MinYear(), .MaxYear() |
TimePicker(time, setTime) |
TimeSpan |
.MinuteIncrement() |
AutoSuggestBox(text, setText) |
string |
.ItemsSource(), .OnSuggestionChosen() |
RichEditBox(doc, setDoc) |
string |
.IsReadOnly() |
CalendarDatePicker(date, setDate) |
DateTimeOffset? |
.MinDate(), .MaxDate() |
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.
Tracks per-field validation messages, touched/dirty state, and overall form validity:
var validation = UseValidationContext();
var (name, setName) = UseState("");
var (email, setEmail) = UseState("");
return VStack(12,
TextBox(name, setName, placeholderText: "Name")
.Validate(validation, "name",
Validate.Required("Name is required"),
Validate.MinLength(2, "Name too short")),
TextBox(email, setEmail, placeholderText: "Email")
.Validate(validation, "email",
Validate.Required("Email is required"),
Validate.Email("Invalid email")),
Button("Submit", () =>
{
validation.ValidateAll();
if (validation.IsValid)
Submit(name, email);
})
);| Member | Purpose |
|---|---|
.IsValid |
true when no field has errors |
.IsDirty |
true when any field differs from initial value |
.ValidateAll() |
Force validation on all registered fields |
.Reset() |
Clear all messages and touched/dirty flags |
.GetMessages("field") |
Get error messages for a specific field |
.IsTouched("field") |
Whether the user has interacted with a field |
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) |
FormField wraps an input with a label, required marker, description
text, and error display:
var validation = UseValidationContext();
var (name, setName) = UseState("");
return FormField("Full Name",
TextBox(name, setName, placeholderText: "Enter your name")
.Validate(validation, "name", Validate.Required("Required")),
required: true,
description: "As it appears on your ID",
showWhen: ShowWhen.WhenTouched // or Always, WhenDirty, AfterFirstSubmit
);ShowWhen controls when error messages appear:
WhenTouched— after the user has interacted with the field (recommended default)Always— immediately, even before user interactionWhenDirty— only after the value has changedAfterFirstSubmit— only after the first submit attempt
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");| Preset | Format |
|---|---|
MaskPreset.PhoneUS |
(___) ___-____ |
MaskPreset.SSN |
___-__-____ |
MaskPreset.ZipCode |
_____ |
MaskPreset.ZipCodePlus4 |
_____-____ |
MaskPreset.CreditCard |
____ ____ ____ ____ |
MaskPreset.Date |
__/__/____ |
Custom masks: new MaskEngine("AA-####") where A = letter,
# = digit, * = any.
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 |
- Always use controlled inputs —
(value, setter)pair. There is no uncontrolled / two-way binding in Reactor. - Call
validation.ValidateAll()before submit — individual fields validate on blur/change, but you must trigger all-field validation before acting on the form. - 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.