Files
Enciphered.Blazor.UIComponents/docs/forms/submission.md
T
2026-04-13 18:57:47 +05:00

6.7 KiB

Form Submission & Model Binding

Handle validated form submissions with strongly-typed models — no manual dictionary access required.


Basic Submit (No Model Binding)

The simplest approach uses an onSuccess callback with direct HttpContext access:

app.MapFormValidation<ContactFormValidator>("/api/forms/contact",
    onSuccess: async ctx =>
    {
        var form = ctx.Request.Form;
        var name = form["name"].ToString();
        var email = form["email"].ToString();

        // Save to database, send email, etc.
        await SaveContactAsync(name, email);
    });

The onSuccess callback fires only after all validation rules pass. If validation fails, the callback is never invoked.


Strongly-Typed Model Binding

Define a POCO model and let FormModelBinder handle the mapping automatically:

Step 1: Create a Model

public class ContactFormModel
{
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
    public int? Age { get; set; }
    public DateOnly? Birthdate { get; set; }
    public TimeOnly? Preferredtime { get; set; }
    public DateTime? Appointment { get; set; }
    public string Confirmation { get; set; } = "";
}

Step 2: Use the Typed Overload

app.MapFormValidation<ContactFormValidator, ContactFormModel>("/api/forms/contact",
    onSuccess: async model =>
    {
        Console.WriteLine($"Name: {model.Name}");
        Console.WriteLine($"Email: {model.Email}");
        Console.WriteLine($"Age: {model.Age}");
        Console.WriteLine($"Birth Date: {model.Birthdate}");
        Console.WriteLine($"Preferred Time: {model.Preferredtime}");
        Console.WriteLine($"Appointment: {model.Appointment}");

        await SaveToDbAsync(model);
    });

Step 3: Customize the Success Message (Optional)

app.MapFormValidation<ContactFormValidator, ContactFormModel>("/api/forms/contact",
    onSuccess: async model => { /* ... */ },
    successMessage: "Thank you! Your form has been submitted.");

FormModelBinder

FormModelBinder.Bind<T>() maps form fields to model properties using reflection with these rules:

  • Case-insensitive matching — form field name matches property Name
  • Automatic type conversion for all common types
  • Nullable support — empty values become null for nullable types

Supported Types

Type Format Expected
string Any text
int, long Integer text
float, double, decimal Numeric text (invariant culture)
bool true/false, on, 1
DateTime Parseable datetime (e.g. 2025-12-25T10:30)
DateOnly Parseable date (e.g. 2025-12-25)
TimeOnly Parseable time (e.g. 14:30)
Guid Standard GUID format
Enum Case-insensitive enum member name

All types support their Nullable<T> equivalents (int?, DateTime?, etc.).


API Reference

MapFormValidation (without model binding)

public static RouteGroupBuilder MapFormValidation<TValidator>(
    this IEndpointRouteBuilder endpoints,
    string basePath,
    string successMessage = "✓ Form submitted successfully!",
    Func<HttpContext, Task>? onSuccess = null)
    where TValidator : FormValidator, new();

MapFormValidation (with model binding)

public static RouteGroupBuilder MapFormValidation<TValidator, TModel>(
    this IEndpointRouteBuilder endpoints,
    string basePath,
    Func<TModel, Task> onSuccess,
    string successMessage = "✓ Form submitted successfully!")
    where TValidator : FormValidator, new()
    where TModel : new();

Parameters

Parameter Type Description
basePath string URL prefix (e.g. /api/forms/contact)
successMessage string HTML text shown on successful submission
onSuccess Func<HttpContext, Task>? or Func<TModel, Task> Callback invoked after validation passes

Return Value

Returns a RouteGroupBuilder for further endpoint configuration if needed.


Registered Endpoints

Both overloads register the same endpoint structure:

Method Path Purpose
POST {basePath}/validate Per-field validation (called on input blur)
POST {basePath}/submit Full form validation and submission

Both endpoints have .DisableAntiforgery() applied since htmx sends raw form data.


Response Format

Validation Error Response

When validation fails, the /submit endpoint returns HTML with OOB (out-of-band) swap fragments:

<p data-field-error="email" hx-swap-oob="outerHTML:[data-field-error='email']"
   class="text-[0.8rem] font-medium text-destructive">
   Please enter a valid email address.
</p>
<p data-field-error="name" hx-swap-oob="outerHTML:[data-field-error='name']"
   class="text-[0.8rem] font-medium text-destructive hidden"></p>
<!-- ... one fragment per field ... -->
<div id="form-result" class="hidden"></div>

htmx processes each OOB fragment, updating every field's error element in a single response.

Success Response

<p data-field-error="email" hx-swap-oob="outerHTML:[data-field-error='email']"
   class="text-[0.8rem] font-medium text-destructive hidden"></p>
<!-- ... clears all error elements ... -->
<div id="form-result">
    <div data-testid="success-message"
         class="rounded-md border border-green-500/30 bg-green-500/10 p-3 text-sm text-green-600 dark:text-green-400">
        ✓ Form submitted successfully!
    </div>
</div>

Complete Example

// ContactFormValidator.cs
using Enciphered.Blazor.UIComponents.Validation;

public class ContactFormValidator : FormValidator
{
    public ContactFormValidator()
    {
        RuleFor("name", required: true, minLength: 2);
        RuleFor("email", required: true,
            pattern: @".+@.+\..+",
            message: "Please enter a valid email address.");
    }
}

// ContactFormModel.cs
public class ContactFormModel
{
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
}

// Program.cs
app.MapFormValidation<ContactFormValidator, ContactFormModel>("/api/forms/contact",
    onSuccess: async model =>
    {
        await db.Contacts.AddAsync(new Contact
        {
            Name = model.Name,
            Email = model.Email
        });
        await db.SaveChangesAsync();
    });
@* ContactForm.razor *@
@page "/contact"

<HtmxForm Endpoint="/api/forms/contact">
    <FormField Label="Name" For="name">
        <TextInput Id="name" Name="name" />
    </FormField>

    <FormField Label="Email" For="email">
        <TextInput Id="email" Name="email" Type="email" />
    </FormField>

    <Button Type="submit">Send</Button>
</HtmxForm>