# Form Validation

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

# Form Validation

Lumeo's `Form` component provides a structured way to build validated forms in Blazor. It wraps your form content in a `<form>` element, manages validation state through a cascading context, and fires separate callbacks for valid and invalid submissions.

Out of the box, Lumeo ships with a `DataAnnotationsFormValidator` that works with standard .NET data annotations. You can also implement the `IFormValidator` interface to plug in any validation strategy.

## How it works

The form system is made up of several components that work together:

-   **Form<TModel>** — The top-level wrapper. Accepts a model, a validator, and submit callbacks. Cascades a `FormContext` to all children.
-   **FormField** — Wraps a single field. Provides a label, help text, error display, and a required indicator. Supports vertical and horizontal orientations.
-   **FormLabel** — Renders a styled `<label>`. Automatically turns red when its parent FormField has an error.
-   **FormDescription** — Displays muted helper text below the input.
-   **FormMessage** — Displays the error message from the FormField context in red. Only renders when there is an error.
-   **FormItem** — An optional grouping container that adds consistent spacing between label, input, and messages.

## DataAnnotations validation

The simplest way to validate forms is with standard .NET data annotation attributes. Create a model class with validation attributes, then pass a `DataAnnotationsFormValidator` instance to the Form component.

### 1\. Define the model

using System.ComponentModel.DataAnnotations; public class ContactModel { \[Required(ErrorMessage = "Name is required.")\] \[StringLength(100, ErrorMessage = "Name cannot exceed 100 characters.")\] public string? Name { get; set; } \[Required(ErrorMessage = "Email is required.")\] \[EmailAddress(ErrorMessage = "Please enter a valid email address.")\] public string? Email { get; set; } \[StringLength(500, ErrorMessage = "Message cannot exceed 500 characters.")\] public string? Message { get; set; } }

### 2\. Build the form

<Form TModel="ContactModel" Model="@\_model" Validator="@\_validator" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit"> <FormField Label="Name" Required="true" Error="@GetError("Name")"> <Input @bind-Value="@\_model.Name" Placeholder="Your full name" /> </FormField> <FormField Label="Email" Required="true" Error="@GetError("Email")"> <Input @bind-Value="@\_model.Email" type="email" Placeholder="you@example.com" /> </FormField> <FormField Label="Message" HelpText="Optional. Let us know how we can help." Error="@GetError("Message")"> <Textarea @bind-Value="@\_model.Message" Placeholder="Your message..." /> </FormField> <Button type="submit">Send</Button> </Form>

### 3\. Wire up the code

@code { private ContactModel \_model = new(); private DataAnnotationsFormValidator \_validator = new(); private string? GetError(string field) { // After validation, errors are populated by the Form component. // You can also access them through the FormContext if needed. var errors = \_validator.ValidateField(\_model, field); return errors.FirstOrDefault(); } private async Task HandleValidSubmit(ContactModel model) { // All validations passed — save the data await SaveContactAsync(model); } private Task HandleInvalidSubmit(ContactModel model) { // Validation failed — errors are displayed automatically return Task.CompletedTask; } }

## Form layout

The `FormField` component supports two layout orientations: vertical (default) and horizontal. You can also control label width in horizontal mode.

### Vertical layout (default)

Labels appear above the input. This is the default and works well for most forms.

<FormField Label="Username" Required="true"> <Input @bind-Value="@\_model.Username" /> </FormField>

### Horizontal layout

Labels appear to the left of the input in a grid. Use `LabelWidth` to control the label column width.

<FormField Label="Username" Required="true" Orientation="Lumeo.Orientation.Horizontal" LabelWidth="120px"> <Input @bind-Value="@\_model.Username" /> </FormField>

### Using FormItem, FormLabel, FormDescription, and FormMessage

For more control over form field layout, you can compose the lower-level components directly instead of relying on `FormField`.

<FormField Name="email" Error="@\_emailError"> <FormItem> <FormLabel>Email address</FormLabel> <Input @bind-Value="@\_model.Email" type="email" /> <FormDescription>We will never share your email.</FormDescription> <FormMessage /> </FormItem> </FormField>

`FormLabel` reads the `FormFieldContext` from its parent `FormField` and automatically turns red when an error is present. `FormMessage` renders the error text only when the context has an error.

## Validation styling

When a field has an error, several visual changes occur automatically:

-   **Error message.** The `FormField` displays a red error message below the input (or below the help text position). `FormMessage` also renders the error from context.
-   **Label color.** The `FormLabel` text turns to the `text-destructive` color when its parent field has an error.
-   **Help text hidden.** When an error is present, the help text is hidden and replaced by the error message.
-   **Required indicator.** Fields with `Required="true"` display a red asterisk next to the label.

All error colors use the `--destructive` CSS variable, so they adapt to your theme automatically.

## Async validation

For checks that have to round-trip the server — username availability, coupon codes, domain DNS — the `FormField` component accepts an `AsyncValidator`: a `Func<string?, Task<string?>>` returning an error message (or `null` on success). It runs alongside the synchronous validator and exposes its in-flight state through `IsValidating` on the field and `IsAnyFieldValidating` on the surrounding `FormContext` — disable the submit button on the latter so users can't race a stale check.

Configure the debounce with `AsyncValidationDebounceMs` (default 300) and the trigger with `AsyncValidateOn` (`OnChange` or `OnBlur`).

<FormField Label="Username" Required="true" AsyncValidator="@CheckUsernameAvailable" AsyncValidationDebounceMs="400" AsyncValidateOn="AsyncValidationTrigger.OnChange"> <Input @bind-Value="\_model.Username" /> </FormField> @code { async Task<string?> CheckUsernameAvailable(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; return await UserApi.IsUsernameTaken(value) ? "That username is taken." : null; } }

See the [FormField "Async validators" section](/components/form-field#async-validators) for the full parameter list and a spinner / submit-disable example.

## FormContext

The `Form` component cascades a `FormContext` object to all child components. This context tracks errors, dirty fields, and submission state.

Member

Type

Description

Errors

Dictionary<string, List<string>>

All current validation errors keyed by field name.

DirtyFields

HashSet<string>

Names of fields that have been modified.

IsSubmitting

bool

True while the submit handler is executing.

IsValid

bool

True when there are no validation errors.

GetFieldErrors(field)

List<string>

Returns all error messages for a specific field.

HasError(field)

bool

Returns true if a specific field has errors.

IsDirty(field)

bool

Returns true if a field has been modified.

MarkDirty(field)

void

Marks a field as dirty (modified).

ClearErrors()

void

Removes all validation errors.

Reset()

void

Clears all errors, dirty fields, and submission state.

## Custom validation

For validation logic beyond data annotations, implement the `IFormValidator` interface. This gives you full control over how validation is performed.

### The IFormValidator interface

public interface IFormValidator { Dictionary<string, List<string>> Validate(object model); List<string> ValidateField(object model, string fieldName); }

### Example: custom validator

public class RegistrationValidator : IFormValidator { public Dictionary<string, List<string>> Validate(object model) { var errors = new Dictionary<string, List<string>>(); if (model is RegistrationModel reg) { if (string.IsNullOrWhiteSpace(reg.Username)) errors\["Username"\] = \["Username is required."\]; if (reg.Password?.Length < 8) errors\["Password"\] = \["Password must be at least 8 characters."\]; if (reg.Password != reg.ConfirmPassword) errors\["ConfirmPassword"\] = \["Passwords do not match."\]; if (reg.DateOfBirth > DateOnly.FromDateTime(DateTime.Today.AddYears(-13))) errors\["DateOfBirth"\] = \["You must be at least 13 years old."\]; } return errors; } public List<string> ValidateField(object model, string fieldName) { var allErrors = Validate(model); return allErrors.TryGetValue(fieldName, out var errors) ? errors : \[\]; } }

### Using the custom validator

<Form TModel="RegistrationModel" Model="@\_model" Validator="@(new RegistrationValidator())" OnValidSubmit="@HandleRegister"> @\* fields here \*@ </Form>

## Complete example

Here is a full registration form that uses multiple Lumeo form components together with DataAnnotations validation.

### Model

using System.ComponentModel.DataAnnotations; public class RegistrationModel { \[Required(ErrorMessage = "Full name is required.")\] \[StringLength(100)\] public string? FullName { get; set; } \[Required(ErrorMessage = "Email is required.")\] \[EmailAddress(ErrorMessage = "Invalid email format.")\] public string? Email { get; set; } \[Required(ErrorMessage = "Password is required.")\] \[MinLength(8, ErrorMessage = "Password must be at least 8 characters.")\] public string? Password { get; set; } \[Required(ErrorMessage = "Please select a role.")\] public string? Role { get; set; } \[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms.")\] public bool AcceptTerms { get; set; } public DateOnly? StartDate { get; set; } }

### Razor markup

<Form TModel="RegistrationModel" Model="@\_model" Validator="@\_validator" OnValidSubmit="@HandleRegister" OnInvalidSubmit="@HandleInvalid"> <Stack Gap="4"> <FormField Label="Full Name" Required="true" Error="@GetError("FullName")"> <Input @bind-Value="@\_model.FullName" Placeholder="Jane Smith" /> </FormField> <FormField Label="Email" Required="true" Error="@GetError("Email")"> <Input @bind-Value="@\_model.Email" type="email" Placeholder="jane@company.com" /> </FormField> <FormField Label="Password" Required="true" HelpText="Must be at least 8 characters." Error="@GetError("Password")"> <PasswordInput @bind-Value="@\_model.Password" /> </FormField> <FormField Label="Role" Required="true" Error="@GetError("Role")"> <Select @bind-Value="@\_model.Role" Placeholder="Select a role"> <SelectItem Value="@("developer")">Developer</SelectItem> <SelectItem Value="@("designer")">Designer</SelectItem> <SelectItem Value="@("manager")">Manager</SelectItem> </Select> </FormField> <FormField Label="Start Date" HelpText="When would you like to start?"> <DatePicker @bind-Value="@\_model.StartDate" /> </FormField> <FormField Error="@GetError("AcceptTerms")"> <Checkbox @bind-Value="@\_model.AcceptTerms" Label="I accept the terms and conditions" /> </FormField> <Flex Gap="3"> <Button type="submit">Register</Button> <Button type="button" Variant="Button.ButtonVariant.Outline" OnClick="@HandleReset">Reset</Button> </Flex> </Stack> </Form>

### Code-behind

@inject ToastService Toast @code { private RegistrationModel \_model = new(); private DataAnnotationsFormValidator \_validator = new(); private string? GetError(string field) { var errors = \_validator.ValidateField(\_model, field); return errors.FirstOrDefault(); } private async Task HandleRegister(RegistrationModel model) { // Simulate an API call await Task.Delay(1000); Toast.Show(new ToastOptions { Title = "Success", Description = $"Welcome, {model.FullName}!", Variant = ToastVariant.Success }); \_model = new(); } private Task HandleInvalid(RegistrationModel model) { Toast.Show(new ToastOptions { Title = "Validation Error", Description = "Please fix the errors above.", Variant = ToastVariant.Error }); return Task.CompletedTask; } private void HandleReset() { \_model = new(); } }

## API reference

### Form<TModel>

Parameter

Type

Default

Description

Model

TModel?

null

The form model instance to validate.

Validator

IFormValidator?

null

The validator to run on submit.

OnValidSubmit

EventCallback<TModel>

—

Called when the form is submitted and validation passes.

OnInvalidSubmit

EventCallback<TModel>

—

Called when the form is submitted and validation fails.

ChildContent

RenderFragment?

null

The form content.

Class

string?

null

Additional CSS classes.

### FormField

Parameter

Type

Default

Description

Label

string?

null

The field label text.

HelpText

string?

null

Descriptive text shown below the input. Hidden when an error is present.

Error

string?

null

Error message to display. When set, replaces help text.

Required

bool

false

Shows a red asterisk next to the label.

Name

string?

null

Field name for context identification.

Orientation

Lumeo.Orientation

Vertical

Layout direction: Vertical or Horizontal.

LabelWidth

string?

null

Label column width in horizontal mode (e.g. "120px").

Class

string?

null

Additional CSS classes.
