# EditForm + Lumeo Validation

Source: https://lumeo.nativ.sh/docs/edit-form

# 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](docs/form-validation) and the [\[LumeoForm\] generator](docs/lumeo-form).

## 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](components/input) / [Select](components/select) / [Combobox](components/combobox) through [FormField](components/form-field):

<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](components/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](components/input) — the primary text/email/password field
-   [Select](components/select) — single-value dropdown
-   [FormField](components/form-field) — label + help text + ValidationMessage wrapper
-   [Button](components/button) — submit button defaults & type semantics
-   [Form Validation](docs/form-validation) — Lumeo's own form primitives
-   [\[LumeoForm\] Generator](docs/lumeo-form) — model-driven form scaffolding
