Lumeo

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 an EditContext.
  • 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 + the Invalid parameter 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