N

Inline validation (422 swap)

Form validation errors render inline under each field without a full page reload. The server returns HTTP 422 with the failed form partial; htmx swaps it back into the form's slot.

#When to use

Every form in your app. This is the default: on a non-htmx request the handler return Page() still re-renders with errors, so the same code powers both flows.

#Markup

html
<form id="product_form"
      hx-post="/products?handler=Save"
      hx-target="#products_table"
      hx-swap="outerHTML"
      hx-disabled-elt="find button[type=submit]">
    <input name="Name" />
    <span class="text-red-500" data-error-for="Name"></span>

    <input name="Price" type="number" step="0.01" />
    <span class="text-red-500" data-error-for="Price"></span>

    <button type="submit">Save</button>
</form>

By default htmx swaps the response into hx-target only on 2xx. To swap the same target on 422 too — so the validation-failed form replaces the original — add this to your _Layout.cshtml once:

html
<script>
  document.body.addEventListener('htmx:beforeSwap', function(e) {
    if (e.detail.xhr.status === 422) {
      e.detail.shouldSwap = true;
      // Re-target to the form itself, not the success target.
      e.detail.target = e.detail.requestConfig.elt;
    }
  });
</script>

That snippet is included in the Studio starter's _Layout.cshtml already.

#Handler

csharp
public IActionResult OnPostSave(ProductEditForm form, CancellationToken ct)
{
    if (!ModelState.IsValid)
    {
        return this.PageOrPartial("_ProductForm", form, AjaxExtensions.UnprocessableEntity);
    }

    _products.Save(form);
    this.AddToast("Saved.", ToastLevel.Success);
    return this.PageOrPartial("_ProductsTable", _products.List());
}

The third argument to PageOrPartial is the status code. 422 is the canonical "validation error" — semantically distinct from 400 (malformed request).

#ASP.NET Core's asp-validation-for works as-is

cshtml
<input asp-for="Name" />
<span asp-validation-for="Name" class="text-red-500"></span>

The standard tag helper renders the inline error message from ModelState. When the 422 swap brings in the new form HTML, the error spans come pre-populated.