# 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 ┌──────────────────────────┐ │ │ │ │ ──────► │ htmx POST /validate │ │ │ └─────────────┘ │ { _field: "email", │ │ │ │ email: "bad" } │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ┌─────────────────┐ ◄──────────┘ │ │ │

│ │ │ swapped by │ Please enter a valid email. │ │ │ htmx │

│ │ └─────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────┐ │ Server (Minimal API) │ │ │ │ FormValidator.ValidateField("email", "bad") │ │ → "Please enter a valid email address." │ │ │ │ HtmxFormValidationRenderer.FieldErrorFragment(...) │ │ → HTML

element with error text │ └──────────────────────────────────────────────────────────┘ ``` --- ## Step 1: Create a Validator Define your validation rules by extending `FormValidator` and calling `RuleFor()` in the constructor: ```csharp 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()`: ```csharp app.MapFormValidation("/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: ```razor

``` --- ## RuleFor API Reference ```csharp 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? 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` — receive the trimmed value, return an error message or `null`: ```csharp RuleFor("confirmation", required: true, custom: value => value != "CONFIRM" ? "You must type CONFIRM to proceed." : null); ``` For date/time/datetime fields, use `TryParse`: ```csharp 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` 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 `

` fragment 4. htmx replaces the existing `

` element with the response ### On Submit (Full Form) 1. `HtmxForm` adds `hx-post="/submit"` to the `

` 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 Reset Clicking a `