Lumeo

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="[email protected]" /> </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 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="[email protected]" /> </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.