Files
2026-04-13 18:57:47 +05:00

9.0 KiB

Form Validation

htmx-powered server-side validation that provides real-time per-field feedback on blur and full-form validation on submit — all without Blazor interactivity.


Architecture Overview

┌──────────────────────────────────────────────────────────┐
│  Browser                                                  │
│                                                           │
│  ┌─────────────┐  blur   ┌──────────────────────────┐   │
│  │ <TextInput>  │ ──────► │ htmx POST /validate      │   │
│  └─────────────┘         │ { _field: "email",        │   │
│                          │   email: "bad" }           │   │
│                          └──────────┬───────────────┘   │
│                                     │                     │
│  ┌─────────────────┐    ◄──────────┘                     │
│  │ <p data-field-   │  HTML fragment:                     │
│  │    error="email"> │  <p class="text-destructive">     │
│  │    swapped by     │    Please enter a valid email.    │
│  │    htmx           │  </p>                              │
│  └─────────────────┘                                     │
└──────────────────────────────────────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────┐
│  Server (Minimal API)                                     │
│                                                           │
│  FormValidator.ValidateField("email", "bad")             │
│  → "Please enter a valid email address."                  │
│                                                           │
│  HtmxFormValidationRenderer.FieldErrorFragment(...)       │
│  → HTML <p> element with error text                       │
└──────────────────────────────────────────────────────────┘

Step 1: Create a Validator

Define your validation rules by extending FormValidator and calling RuleFor() in the constructor:

using Enciphered.Blazor.UIComponents.Validation;

public class ContactFormValidator : FormValidator
{
    public ContactFormValidator()
    {
        RuleFor("name",
            displayName: "Name",
            required: true,
            minLength: 2);

        RuleFor("email",
            displayName: "Email",
            required: true,
            pattern: @".+@.+\..+",
            message: "Please enter a valid email address.");

        RuleFor("password",
            displayName: "Password",
            required: true,
            minLength: 6);

        RuleFor("age",
            displayName: "Age",
            min: 0,
            max: 150);

        RuleFor("birthdate",
            displayName: "Birth Date",
            custom: value => !DateOnly.TryParse(value, out _)
                ? "Please enter a valid date."
                : null);

        RuleFor("preferredtime",
            displayName: "Preferred Time",
            custom: value => !TimeOnly.TryParse(value, out _)
                ? "Please enter a valid time."
                : null);

        RuleFor("appointment",
            displayName: "Appointment",
            custom: value => !DateTime.TryParse(value, out _)
                ? "Please enter a valid date and time."
                : null);

        RuleFor("confirmation",
            displayName: "Confirmation",
            required: true,
            custom: value => value != "CONFIRM"
                ? "You must type CONFIRM to proceed."
                : null);
    }
}

Step 2: Register Validation Endpoints

In Program.cs, call MapFormValidation<T>():

app.MapFormValidation<ContactFormValidator>("/api/forms/contact");

This registers two endpoints:

Endpoint Method Purpose
POST /api/forms/contact/validate Per-field Validates a single field on blur
POST /api/forms/contact/submit Full form Validates all fields on submit

Both endpoints have antiforgery disabled (via .DisableAntiforgery()) since htmx sends form data directly.


Step 3: Build the Form

Use HtmxForm, FormField, and input components:

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

    <FormField Label="Email" For="email">
        <TextInput Id="email" Name="email" Type="email" Placeholder="jane@example.com" />
    </FormField>

    <FormField Label="Password" For="password">
        <TextInput Id="password" Name="password" Type="password" />
    </FormField>

    <FormField Label="Age" For="age">
        <NumberInput Id="age" Name="age" Min="0" Max="150" />
    </FormField>

    <FormField Label="Birth Date" For="birthdate">
        <DateInput Id="birthdate" Name="birthdate" />
    </FormField>

    <div class="flex gap-2 pt-2">
        <Button Type="submit">Submit</Button>
        <Button Type="reset" Variant="@ButtonVariant.Outline">Reset</Button>
    </div>
</HtmxForm>

RuleFor API Reference

protected void RuleFor(
    string field,             // Form field name (must match the input's Name)
    string? displayName,      // Human-readable label (auto-generated from field if omitted)
    bool required,            // Whether the field is required
    int? minLength,           // Minimum string length
    int? maxLength,           // Maximum string length
    string? pattern,          // Regex pattern for format validation
    double? min,              // Minimum numeric value
    double? max,              // Maximum numeric value
    string? message,          // Custom error message for pattern failures
    Func<string, string?>? custom  // Custom validation function
);

Validation Order

Rules are evaluated in this order — the first failure stops evaluation:

  1. Required — empty/whitespace check
  2. Empty skip — if not required and value is empty, the field passes (skips remaining rules)
  3. MinLength — minimum character count
  4. MaxLength — maximum character count
  5. Pattern — regex match (uses message if provided, else default format error)
  6. Min/Max — numeric range (attempts to parse as double)
  7. Custom — arbitrary validation function returning an error string or null

Custom Validators

The custom parameter accepts a Func<string, string?> — receive the trimmed value, return an error message or null:

RuleFor("confirmation",
    required: true,
    custom: value => value != "CONFIRM"
        ? "You must type CONFIRM to proceed."
        : null);

For date/time/datetime fields, use TryParse:

RuleFor("birthdate",
    custom: value => !DateOnly.TryParse(value, out _)
        ? "Please enter a valid date."
        : null);

Note

: Custom validators only run when the value is non-empty. If the field is not required and left blank, the custom function is never called.


How It Works

On Blur (Per-Field)

  1. InputBase<T> auto-injects htmx attributes when inside HtmxForm + FormField
  2. When the user leaves an input, htmx fires POST /validate with the field name and value
  3. The server calls FormValidator.ValidateField() and returns an HTML <p> fragment
  4. htmx replaces the existing <p data-field-error="..."> element with the response

On Submit (Full Form)

  1. HtmxForm adds hx-post="/submit" to the <form> element
  2. htmx sends all form fields
  3. The server calls FormValidator.ValidateAll() and returns:
    • If errors: OOB (out-of-band) swap fragments for each field's error element
    • If valid: Success message + OOB swaps to clear all errors

HtmxForm Parameters

Parameter Type Default Description
Endpoint string required Base path (e.g. /api/forms/contact)
ResultId string "form-result" ID of the result div for success/error messages
Class string? Additional CSS classes on the <form>

Form Reset

Clicking a <Button Type="reset"> triggers the browser's native form reset. The forms.js module listens for the reset event and:

  • Clears all visible input values
  • Hides all [data-field-error] elements
  • Hides the result div
  • Resets date/time trigger button text to their placeholders