Skip to content

Latest commit

 

History

History
285 lines (220 loc) · 7.63 KB

File metadata and controls

285 lines (220 loc) · 7.63 KB

Common recipes

These recipes combine ordinary HTMX behavior with HtmxToolkit's routing, request, response, and security APIs. Adapt partial names and persistence code to the application.

Return validation errors to another target

Post a form whose normal target is the saved profile:

<form hx-post
      hx-page="/Profile"
      hx-page-handler="Save"
      hx-target="#profile">
    <div id="validation-errors"></div>
    <input asp-for="Input.DisplayName" />
    <button type="submit">Save</button>
</form>

<section id="profile"></section>

Retarget only the invalid response:

public IActionResult OnPostSave()
{
    if (!ModelState.IsValid)
    {
        Response.Htmx(htmx => htmx
            .Retarget("#validation-errors")
            .Reswap(HtmxSwap.InnerHtml));

        return Partial("_ValidationSummary", ModelState);
    }

    profiles.Save(Input);
    return Partial("_Profile", Input);
}

Automatic antiforgery requires the layout setup from Antiforgery and Toolkit script.

Refresh another component after saving

The server can dispatch a named event without custom JavaScript:

public IActionResult OnPostSave(ProfileInput input)
{
    profiles.Save(input);

    Response.Htmx(htmx => htmx.TriggerEvent(
        "profile-saved",
        new { input.Id },
        HtmxTriggerTiming.AfterSwap));

    return Partial("_SaveResult", input);
}

Another element listens for the event and reloads itself:

<aside id="profile-summary"
       hx-page="/Profile"
       hx-page-handler="Summary"
       hx-trigger="profile-saved from:body">
    @await Html.PartialAsync("_ProfileSummary", Model.Profile)
</aside>

This keeps the server response in control while allowing independently targeted components to stay synchronized.

Poll a background operation

For polling that works with every supported HTMX version, return the polling element itself and replace it with outerHTML. Each replacement element schedules the next request with load delay:1s while work is incomplete:

@model ProgressState

@if (Model.Completed)
{
    <div id="job-progress">Complete</div>
}
else
{
    <div id="job-progress"
         hx-page="/Jobs/Status"
         hx-page-handler="Progress"
         hx-route-progress="@Model.Percent"
         hx-trigger="load delay:1s"
         hx-target="this"
         hx-swap="outerHTML">
        @Model.Percent%
    </div>
}
public IActionResult OnGetProgress(int progress)
{
    var next = Math.Clamp(progress + 10, 0, 100);
    return Partial("_Progress", new ProgressState(next));
}

Polling stops when the server returns the completed element without the request and trigger attributes. This pattern does not depend on status code 286, whose behavior differs between HTMX versions.

Handle boosted navigation progressively

Start with a real link so navigation works without JavaScript:

<a asp-page="/Catalog"
   asp-route-category="books"
   hx-boost="true"
   hx-target="#main">
    Books
</a>

Return a fragment for the boosted request and a page otherwise:

public IActionResult OnGet(string category)
{
    Products = catalog.List(category);

    if (Request.IsHtmxBoosted())
        return Partial("_Catalog", Products);

    return Page();
}

If the same URL participates in history restoration, use the fuller check from Full pages and fragments.

Redirect after authentication

HTMX does not automatically turn an ordinary server redirect into a full browser navigation in every workflow. Send HX-Redirect for the HTMX path and preserve a normal redirect fallback:

public IActionResult SignIn(LoginInput input)
{
    if (!auth.TrySignIn(input))
        return Unauthorized();

    if (Request.IsHtmxRequest())
    {
        Response.Htmx(htmx => htmx.Redirect("/dashboard"));
        return Ok();
    }

    return Redirect("/dashboard");
}

The HTMX response uses status 200 so HTMX can process HX-Redirect. A 3xx response would be followed by the browser internally, hiding the intermediate HX-Redirect header from HTMX. The normal request still uses the conventional ASP.NET Core redirect.

Load more items

Append a fragment and let the returned markup contain the next cursor:

<button hx-page="/Orders"
        hx-page-handler="More"
        hx-route-after="@Model.NextCursor"
        hx-target="#orders"
        hx-swap="beforeend">
    Load more
</button>

The returned partial should contain only new rows. Replace or remove the button separately, for example with an out-of-band element, when there is no next page.

Search while typing

Use an input event with a delay so the server receives a request after the user pauses:

<label for="catalog-search">Search products</label>
<input id="catalog-search"
       name="query"
       type="search"
       hx-page="/Catalog"
       hx-page-handler="Search"
       hx-trigger="input changed delay:300ms, search"
       hx-target="#search-results"
       hx-request-timeout="5000" />

<div id="search-results" aria-live="polite"></div>
public IActionResult OnGetSearch(string? query)
{
    IReadOnlyList<Product> matches = string.IsNullOrWhiteSpace(query)
        ? []
        : catalog.Search(query);

    return Partial("_SearchResults", matches);
}

The URL and timeout are generated by HtmxToolkit; the trigger and request synchronization behavior belong to HTMX. For expensive searches, add cancellation and query limits on the server even when the client uses a delay.

Edit a table row inline

Load an edit partial into the row itself:

<tr id="product-@product.Id">
    <td>@product.Name</td>
    <td>@product.Price</td>
    <td>
        <button hx-page="/Products"
                hx-page-handler="Edit"
                hx-route-id="@product.Id"
                hx-target="closest tr"
                hx-swap="outerHTML">
            Edit
        </button>
    </td>
</tr>

The edit partial posts back to another handler with the same target:

@model ProductInput

<tr id="product-@Model.Id">
    <td colspan="3">
        <form hx-post
              hx-page="/Products"
              hx-page-handler="Save"
              hx-target="closest tr"
              hx-swap="outerHTML">
            <input asp-for="Id" type="hidden" />
            <input asp-for="Name" />
            <input asp-for="Price" />
            <button type="submit">Save</button>
        </form>
    </td>
</tr>

On invalid input, return the edit partial with validation messages. On success, return the display-row partial. Keep authorization and concurrency checks in both handlers; route values and hidden inputs are client-controlled.

Update related elements out of band

An endpoint can return its normal target plus another element marked for an out-of-band swap:

@model AddToCartResult

<div id="product-actions-@Model.ProductId">
    Added to cart.
</div>

<span id="cart-count" hx-swap-oob="true">
    @Model.CartCount
</span>

This is useful when one operation changes a row and a page-level count. For HTMX 2.x, AllowNestedOobSwaps controls whether nested out-of-band elements are processed. HTMX 4.x also has AllowEmptySwapAfterOob for responses that contain only out-of-band content.

Return an error without replacing content

Choose the behavior at the HTMX configuration level when it should apply to the whole application. HTMX 2.x uses ResponseHandling; HTMX 4.x uses NoSwap. For an endpoint-specific validation response, Retarget and Reswap usually make the intent clearer.

See the HTMX 2.x and HTMX 4.x configuration guides.