Oxpecker documentation

Oxpecker.Htmx

Oxpecker.Htmx extends Oxpecker.ViewEngine package with HTMX 4 attributes and headers.

This release targets htmx 4.0 (GA). It is a clean break from the htmx 2.x API — there are no compatibility shims or aliases for removed features.

Nuget package dotnet add package Oxpecker.Htmx

Each htmx attribute is exposed as a fluent extension method on HtmlTag, so attributes chain directly onto a tag with zero allocation overhead. The { children } builder syntax still works at the end of the chain:

open Oxpecker.ViewEngine
open Oxpecker.Htmx

let renderForm q =
    form(action="/contacts", method="get") {
        label(for'="search") { "Search Term" }
        input(id="search", type'="search", name="q", value=q, style="margin: 0 5px", autocomplete="off")
            .hxGet("/contacts")
            .hxTrigger("search, keyup delay:200ms changed")
            .hxTarget("tbody")
            .hxPushUrl("true")
            .hxIndicator("#spinner")
        img(id="spinner", class'="spinner htmx-indicator", src="/spinning-circles.svg", alt="Request In Flight...")
        input(type'="submit", value="Search").hxOn("click", "alert('clicked')")
    }

Documentation

Please refer to the official HTMX 4 site for the documentation.

You can check ContactApp sample in the repository to get a better understanding of how the code will look like in your application.

API

Client side — Core attributes

After opening Oxpecker.Htmx namespace you’ll get access to HTMX attributes:

Client side — Additional attributes

Client side — Event handler

Client side — Built-in extensions

htmx 4 ships a set of opt-in extensions that you load client-side with their own <script> tags. Open the Oxpecker.Htmx.Extensions namespace to get fluent extension methods for the attributes each one introduces:

open Oxpecker.Htmx
open Oxpecker.Htmx.Extensions
Extension Method(s) Renders
hx-sse hxSseConnect, hxSseClose hx-sse:connect, hx-sse:close
hx-ws hxWsConnect, hxWsSend hx-ws:connect, hx-ws:send
hx-multipart hxMultipartConnect, hxMultipartClose hx-multipart:connect, hx-multipart:close
hx-head hxHead (see HxHeadMode) hx-head
hx-targets hxTargets hx-targets
hx-ptag hxPtag hx-ptag
hx-browser-indicator hxBrowserIndicator hx-browser-indicator
hx-history-cache hxHistory hx-history
hx-csp hxNonce hx-nonce
hx-live hxLive, hxLiveBind (see HxLiveBinding) hx-live, hx-live:{name}
hx-prompt hxPrompt hx-prompt
hx-pending hxPending hx-pending
hx-preload hxPreload (lives in the core Oxpecker.Htmx namespace; the extension script is still required) hx-preload

The hx-download and hx-upsert extensions reuse hxSwap. Use HxSwapMethod.download for downloads, and HxSwapMethod.upsert with the HxUpsertModifier helpers (sort, sortDesc, prepend, key) for upserts.

// hx-sse: persistent stream, append messages
div().hxSseConnect("/log").hxSwap("beforeend") { h3() { "Log:" } }

// hx-ws: connect + send a form over the socket
div().hxWsConnect("/chatroom") {
    form().hxWsSend(true) {
        input(name="message")
        button(type'="submit") { "Send" }
    }
}

// hx-targets: swap the response into every matching element
button().hxGet("/api/notification").hxTargets(".alert-box") { "Refresh All" }

// hx-download: save the response as a file
button().hxGet("/files/report.pdf").hxSwap(HxSwapMethod.download) { "Download" }

// hx-upsert: update-or-insert keyed list items, sorted descending
div().hxGet("/items").hxSwap(HxSwapMethod.upsert + HxUpsertModifier.sortDesc) { "items" }

// hx-multipart: stream parts from a persistent connection until a part fires the "done" event (HX-Trigger: done)
div().hxMultipartConnect("/events").hxMultipartClose("done") { "Waiting..." }

// hx-prompt: ask before deleting, answer arrives as the HX-Prompt request header
button().hxDelete("/item/1").hxPrompt("Reason?") { "Delete" }

// hx-pending: show a template while the request is in flight
form().hxPost("/message").hxTarget("#messages").hxSwap("beforeend").hxPending("#sending") {
    input(name="body")
    button() { "Send" }
}

// hx-live: bind an attribute to a reactive expression
button().hxLiveBind("disabled", "!q('#name').value") { "Save" }
// hx-live: special bindings — textContent and a single class toggle
span().hxLiveBind(HxLiveBinding.text, "q('#count').value")
div().hxLiveBind(HxLiveBinding.toggleClass "active", "q('#tab').value === 'a'") { "tab" }

Client side — Modifier helpers

Inheritable attributes accept an optional modifiers string that is appended verbatim after the attribute name — you must include the leading :. Pass htmx 4 modifier syntax directly, e.g. ":inherited", ":inherited:append", or for hxDisable also ":merge" / ":merge:inherited". The HxModifier module provides constants for the most common ones. hxStatus writes status-coded attributes (hx-status:CODE) and takes the code as its first argument.

// Explicit inheritance: renders hx-boost:inherited="true"
body().hxBoost("true", HxModifier.inherited) { ... }

// Inherited append: renders hx-include:inherited:append=".extra"
form().hxInclude(".extra", HxModifier.inherited + HxModifier.append) { ... }

// Merge: renders hx-disable:merge="find button"
main().hxDisable("find button", HxModifier.merge) { ... }

// Merge + inherited: renders hx-disable:merge:inherited="find button"
main().hxDisable("find button", ":merge:inherited") { ... }

// Status-code swap: renders hx-status:422="swap:innerHTML target:#errors"
form().hxPost("/save").hxStatus("422", "swap:innerHTML target:#errors") { ... }

// Wildcard status: renders hx-status:5xx="swap:none"
div().hxGet("/data").hxStatus("5xx", "swap:none") { ... }

Client side — Extended selectors

Build htmx 4 extended selectors in a type-safe way via the HxSelector module instead of writing them as plain strings. Pass the result into any selector-typed attribute (hxTarget, hxSelect, hxSelectOob, hxIndicator, hxInclude, hxDisable).

button().hxDelete("/item/1").hxTarget(HxSelector.closest ".card") { "Delete" }
div().hxGet("/user").hxTarget(HxSelector.find ".username") { "Loading" }
button().hxGet("/data").hxTarget(HxSelector.nextSibling) { "Load" }
button().hxGet("/data").hxTarget("#a, #b") { "Load" }   // multiple targets: just write the comma-separated string

Available helpers:

Server side — Request headers

Server side — Response headers

To produce hx-multipart responses on the server, use the WriteMultipart / WriteMultipartChunked extension methods (or the multipart / multipartChunked handlers) from the Oxpecker package. Part headers take the same constants, e.g. MultipartPart.Html(view, headers = [ HxResponseHeader.Retarget, "#status" ]). See Writing Multipart.

Migration from Oxpecker.Htmx 2.x

Removed attributes (no htmx 4 equivalent)

Renamed attributes

Moved to extensions

Removed request headers

Removed response headers

Migration from Oxpecker.Htmx 4.0.0-beta (htmx 4 beta4 → 4.0.0 GA)

Removed

Added