EditForm + Lumeo Validation
Lumeo inputs work seamlessly inside Blazor's built-in
<EditForm>
with DataAnnotationsValidator.
This guide explains why Lumeo inputs are not InputBase<T>
derivatives, the canonical wiring pattern, and the few sharp edges to know
about. If you'd rather use Lumeo's own form primitives, see
Form Validation
and the [LumeoForm] generator.
Why Lumeo inputs aren't InputBase<T>
This is a deliberate design choice. Lumeo inputs use plain
Value +
ValueChanged two-way bindings.
That means:
- They work universally — inside or outside
EditForm, with or without anEditContext. - They never throw "InputBase requires a cascading EditContext" on consumers who don't use forms.
- They compose cleanly with reactive state managers (signals, observables, plain fields).
- You opt into validation by adding
EditForm+DataAnnotationsValidator+ theInvalidparameter described below.
EditForm + Lumeo — the canonical pattern
Use Blazor's EditForm for the lifecycle and
DataAnnotationsValidator for the rules. Wire each
Input /
Select /
Combobox through
FormField:
<EditForm Model="@_form" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<Stack Gap="3">
<FormField Label="Email">
<Input @bind-Value="_form.Email" Invalid="@HasError(nameof(_form.Email))" />
<ValidationMessage For="() => _form.Email" />
</FormField>
<FormField Label="Role">
<Select @bind-Value="_form.Role" Invalid="@HasError(nameof(_form.Role))">
<SelectItem Value="admin">Admin</SelectItem>
<SelectItem Value="member">Member</SelectItem>
</Select>
<ValidationMessage For="() => _form.Role" />
</FormField>
<Button type="submit">Save</Button>
</Stack>
</EditForm>
@code {
private MyForm _form = new();
[CascadingParameter] private EditContext? _editContext { get; set; }
private bool HasError(string fieldName) =>
_editContext?.GetValidationMessages(_editContext.Field(fieldName)).Any() == true;
private void HandleSubmit() { /* call API */ }
private class MyForm
{
[Required, EmailAddress] public string? Email { get; set; }
[Required] public string? Role { get; set; }
}
}Per-field invalid styling
Every Lumeo input accepts an Invalid="bool"
parameter that toggles the destructive ring + border treatment. Wire it to
EditContext.GetValidationMessages(field):
private bool HasError(string fieldName) =>
_editContext?.GetValidationMessages(_editContext.Field(fieldName)).Any() == true;
// usage:
<Input @bind-Value="_form.Email" Invalid="@HasError(nameof(_form.Email))" />
Inject the cascading EditContext via
[CascadingParameter] EditContext? _editContext { get; set; }
— EditForm cascades it automatically.
Submit button
Lumeo's Button defaults to
type="button" as of rc.21 (the previous
browser default of type="submit" caused silent
submits when placed inside an EditForm). You must
explicitly opt in to submit semantics by passing type="submit"
— Lumeo forwards unmatched attributes to the underlying <button>:
<!-- correct: explicit submit attribute (forwarded via AdditionalAttributes) --> <Button type="submit">Save</Button> <!-- wrong: rc.21 default is type="button", so this won't trigger EditForm.OnValidSubmit --> <Button OnClick="HandleSubmit">Save</Button>
EventCallback nullable mismatch
When you bind a Select to a nullable property and pass a method group as
ValueChanged, Razor may infer the non-nullable
EventCallback<T> instead of
EventCallback<T?>, producing a CS8622 warning
(or build error under strict nullability). Use an explicit lambda to pin the type:
<!-- bug: Razor infers EventCallback<string>, but _form.Role is string? -->
<Select Value="_form.Role" ValueChanged="OnRoleChanged" />
<!-- fix: explicit lambda pins the nullable type -->
<Select Value="_form.Role"
ValueChanged="@((string? v) => OnRoleChanged(v))" />
@code {
private void OnRoleChanged(string? newValue) { _form.Role = newValue; }
}Server-side validation errors
After OnValidSubmit, if your API returns
field-level errors (e.g. "email already taken"), push them into the
EditContext's
ValidationMessageStore. The
HasError helper above will pick them up
automatically and re-render every affected input with the destructive ring:
private async Task HandleSubmit()
{
var result = await Api.SaveAsync(_form);
if (!result.IsSuccess && _editContext is not null)
{
var store = new ValidationMessageStore(_editContext);
foreach (var (field, errors) in result.FieldErrors)
{
store.Add(_editContext.Field(field), errors);
}
_editContext.NotifyValidationStateChanged();
}
}See also
- Input — the primary text/email/password field
- Select — single-value dropdown
- FormField — label + help text + ValidationMessage wrapper
- Button — submit button defaults & type semantics
- Form Validation — Lumeo's own form primitives
- [LumeoForm] Generator — model-driven form scaffolding