wire

package
v1.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 9 Imported by: 1

Documentation

Overview

Package wire is the transport-agnostic wiring contract shared by the HTMX and Datastar integrations. One typed Action describes a client-initiated hypermedia exchange (method, URL, triggering event, target region); Attributes renders it as the attribute dialect of the configured transport.

The one-object rule

All Datastar fetch options travel in ONE options object literal — {selector: '…', mode: '…', contentType: 'form'} — never several. The pinned runtime's action dispatcher invokes the action with spread arguments and reads exactly one options object, so a second object argument is silently ignored (the earlier multi-object shape dropped form encoding whenever a Selector was also set). datastarActionExpr upholds the rule; any new option must merge into the same literal.

URL templates

URLs are static strings, but components that substitute per-render values (forms.Calendar's month navigation) clone the consumer's Action and rewrite {year}/{month} placeholders in the URL before rendering — see forms/calendar_nav.go. Consumers can adopt the same convention for their own per-item wiring: keep the Action static in props, expand placeholders at render time. Placeholders are a CONVENTION, not parsed by this package — an unexpanded "{year}" renders verbatim.

The common subset

The contract covers only the dialects' common subset (ADR-0036), extended twice by explicit ADR: ContentType/DebounceMS (ADR-0038) and the typed interval/reveal trigger language (ADR-0043). Transport-specific machinery stays in the htmx and datastar modules. See docs/transport-wiring.md for the full consumer guide.

Servers

Handler turns one endpoint into a both-transports endpoint: Datastar callers get response-header targeting, htmx and plain callers pass through. DecodeForm decodes the request body (or query) into a typed struct for either dialect.

Index

Examples

Constants

View Source
const (
	HeaderDatastarRequest           = "Datastar-Request"
	HeaderDatastarSelector          = "Datastar-Selector"
	HeaderDatastarMode              = "Datastar-Mode"
	HeaderDatastarUseViewTransition = "Datastar-Use-View-Transition"
	HeaderHXRequest                 = "Hx-Request"
)

Headers that let one HTTP handler serve both transports. A datastar fetch marks itself with HeaderDatastarRequest; its non-SSE HTML response is patched into the region named by HeaderDatastarSelector using the merge mode in HeaderDatastarMode (verified against the pinned v1.0.2 runtime — see docs/datastar-runtime-facts.md). htmx marks its requests with HeaderHXRequest and targets client-side via hx-target.

Variables

View Source
var ErrUnsupportedFormField = errors.New("unsupported form field kind")

ErrUnsupportedFormField reports a DecodeForm target field whose kind the decoder cannot fill (only string, int, int64, and bool are supported).

Functions

func ContentTypeIsValid added in v1.13.3

func ContentTypeIsValid(c ContentType) bool

ContentTypeIsValid reports whether c is a defined content type (the zero value counts as defined: it means "use the runtime default").

func DecodeForm added in v1.17.0

func DecodeForm[T any](r *http.Request) (T, error)

DecodeForm decodes a submitted HTML form into T. It is the server-side counterpart of every wire.Action that submits form-encoded data: fields opt in via `form:"name"` struct tags (the same names the wired component sends), untagged fields keep their zero values, and absent or empty fields also keep their zero values. Both POST bodies and GET query parameters are read (http.Request.ParseForm semantics: body values take precedence).

A field that is present but malformed (e.g. a non-numeric value for an int field) fails with an error naming the field; a field of an unsupported kind fails with ErrUnsupportedFormField. Domain validation (required fields, ranges, negative indices) stays with the caller — DecodeForm only decodes.

display.ParseKanbanMove is the in-repo example: it decodes via
DecodeForm, then applies the board's own rules.

func EventIsValid

func EventIsValid(e Event) bool

EventIsValid reports whether e is a defined event (the zero value counts as defined: it means "use the dialect default").

func Handler

func Handler(target PatchTarget, next http.Handler) http.Handler

Handler wraps next so one endpoint serves every caller of a wired fragment:

  • Datastar callers receive HeaderDatastarSelector/HeaderDatastarMode response headers targeting target — Datastar fetch actions have no client-side target option, so the response owns the targeting.
  • htmx and plain callers pass through untouched: htmx targets client-side via hx-target, and a plain navigation just renders the fragment.

An empty target.Selector degrades gracefully: no routing headers are set, so the Datastar runtime falls back to its default id-matched patching (a fragment root whose id equals the caller's element id).

mux.Handle("/api/fragment", wire.Handler(wire.PatchTarget{
	Selector: "#out",
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	componentOr500(w, r, fragment())
})))

func IsDatastar

func IsDatastar(r *http.Request) bool

IsDatastar reports whether the request was issued by the Datastar runtime (any fetch action marks itself with HeaderDatastarRequest).

func IsHTMX

func IsHTMX(r *http.Request) bool

IsHTMX reports whether the request was issued by htmx (every htmx AJAX request carries HeaderHXRequest).

func MethodIsValid

func MethodIsValid(m Method) bool

MethodIsValid reports whether m is a defined method (the zero value counts as defined: it means "use the GET default").

func NormalizedInterval added in v1.20.0

func NormalizedInterval(every string) string

NormalizedInterval converts a duration string into the spelling both runtime trigger engines parse correctly: "<n>ms", "<n>s", or a bare number (milliseconds).

Why m/h are converted: neither parser handles them — htmx's parseInterval and Datastar's duration parser both fall through to parseFloat and read the leading digits as MILLISECONDS ("5m" → 5ms, "1h" → 1ms), so a natural-looking "every 5m" would poll five times a SECOND... five times a millisecond. Minutes and hours become seconds. Invalid or exotic values pass through verbatim — the consumer's explicit garbage stays theirs.

func PatchModeIsValid

func PatchModeIsValid(m PatchMode) bool

PatchModeIsValid reports whether m is a merge mode of the pinned Datastar runtime (the zero value counts as defined: it means "use the inner default").

func TransportIsValid

func TransportIsValid(t Transport) bool

TransportIsValid reports whether t is a defined transport (the zero value counts as defined: it means "use the library default").

Types

type Action

type Action struct {
	// Transport selects the attribute dialect. Zero value renders as htmx.
	Transport Transport
	// Method is the HTTP verb. Zero value renders as GET in BOTH dialects
	// (hx-get / @get): an Action literal that omits Method silently turns a
	// write into a read. Set Method explicitly on every mutating action
	// (usually MethodPost); rely on the default only for reads (filters,
	// searches, fragment loads).
	Method Method
	// URL is the exchange endpoint. An empty URL wires nothing (Attributes
	// returns nil) — a component rendered without a backend endpoint must
	// stay inert, not emit a broken binding.
	URL string
	// Event is the triggering DOM event. Zero value uses the dialect default.
	Event Event
	// Target is the region to patch. Under htmx it renders as hx-target.
	// Under Datastar it is intentionally NOT rendered: Datastar v1.0.2 fetch
	// actions accept no target option — targeting is response-driven (see
	// HeaderDatastarSelector) or id-matched (a fragment root whose id equals
	// the target id patches it in the default outer mode). Handlers honor it
	// by echoing the selector back on HeaderDatastarSelector.
	Target string
	// ContentType selects how field values travel to the server. Under
	// Datastar, ContentTypeForm renders {contentType: 'form'} so the action
	// serializes the enclosing form's fields (validation gate, submitter
	// name/value, enctype-aware body); the zero value and ContentTypeJSON use
	// the runtime default (signals as JSON). htmx ignores this field — hx-*
	// requests serialize the enclosing form natively either way.
	ContentType ContentType
	// Selector selects which form serializes under Datastar ContentTypeForm:
	// rendered as the fetch option {selector: '<sel>'}, the runtime resolves
	// the enclosing form via that selector instead of the action element's
	// closest form (bundle-decoded: the option is read ONLY in the
	// form-encoding branch). It does NOT target patches — the pinned runtime
	// reads patch targeting exclusively from the datastar-selector response
	// header (browser-proven 2026-10-02: a client {selector} left a
	// header-less patch inert; see docs/datastar-runtime-facts.md) or from the
	// fragment root's id (outer/replace default). Use wire.Handler(PatchTarget)
	// server-side instead. Empty renders nothing (closest form).
	Selector string
	// Swap is the region-merge style for the exchange — how the response is
	// inserted relative to the target. It reuses the server-side PatchMode
	// vocabulary (the SAME enum as PatchTarget.Mode). Like Target, its effect
	// is transport-asymmetric (ADR-0036): under htmx it renders hx-swap
	// (innerHTML/outerHTML/afterbegin/…) client-side; under Datastar the
	// runtime reads the merge mode EXCLUSIVELY from the datastar-mode response
	// header (browser-proven 2026-10-02: a client {mode} option is ignored —
	// earlier bundles' contrary claim was a string-level misread, corrected in
	// docs/datastar-runtime-facts.md), so Swap renders nothing there and the
	// wire.Handler(PatchTarget.Mode) owns the mode. The zero value renders
	// nothing: htmx's default swap and the wire.Handler default mode are both
	// "inner", so an unspecified Swap behaves identically under both runtimes.
	//
	// Six modes map 1:1 (inner, outer, before, append, prepend, after, and
	// remove ↔ htmx delete); PatchModeReplace (Datastar-only) degrades to the
	// closest htmx style (outerHTML). Unknown values render nothing.
	Swap PatchMode
	// DebounceMS delays the wired exchange until the event has stopped
	// firing for this many milliseconds — the auto-submit filter-input
	// pattern. htmx renders it as the delay:<n>ms trigger modifier (plus
	// `changed` on value events — input, change, keyup — so an unchanged
	// value never re-requests); Datastar renders it as the __debounce.<n>ms
	// event modifier (spelling decoded from the pinned v1.0.3 bundle — see
	// docs/datastar-runtime-facts.md). Zero (the default) emits no debounce.
	// The modifier requires an explicit Event in BOTH dialects: with no
	// event trigger it is dropped (htmx cannot hang a delay on its implicit
	// default trigger; Datastar matches so the modifier never silently
	// re-targets the defaulted click).
	DebounceMS int
	// ThrottleMS bounds the wired exchange to at most one per this many
	// milliseconds — the rate-limit sibling of DebounceMS (throttle fires
	// immediately and then at most once per window; debounce waits for quiet).
	// htmx renders it as the throttle:<n>ms trigger modifier; Datastar as the
	// __throttle.<n>ms attribute-name modifier (bundle-decoded from the pinned
	// v0.6.1 bundle: duration parsing identical to debounce; the runtime's
	// noleading/trailing sub-modifiers are not modeled — the htmx-compatible
	// leading+trailing default is kept). Requires an explicit Event in both
	// dialects, like DebounceMS — an orphan modifier is dropped. htmx
	// documents delay and throttle as mutually
	// exclusive; setting both renders both (Datastar pipelines them
	// delay-then-throttle) — prefer one.
	ThrottleMS int
	// PreventDefault suppresses the triggering event's native default action
	// in the Datastar dialect (the __prevent event modifier). The runtime
	// auto-preventDefaults ONLY form+submit — decoded from the pinned v1.0.3
	// bundle — so a wired <a href> would otherwise BOTH patch and navigate,
	// and a wired submit button would double-submit. Set it on actions wired
	// onto anchors whose href is the no-JS fallback (NavLink, Calendar
	// MonthNav): it is the semantic twin of htmx's automatic interception of
	// hx-* clicks, which is why htmx ignores this field. Do NOT set it on
	// checkbox/radio/change wirings: preventDefault there would cancel the
	// native toggle/commit that htmx keeps.
	PreventDefault bool
	// Interval polls the endpoint on a repeating duration ("10s", "500ms",
	// "2m", "1h") — the polling pattern. NormalizedInterval converts m/h to
	// seconds (both runtimes would misparse them as milliseconds) and an
	// empty Interval means no polling. htmx renders it as the "every <n>"
	// hx-trigger token; Datastar as the data-on-interval__duration.<n>
	// attribute (bundle-decoded grammar). When Interval or Reveal is set and
	// Event is not, no event trigger is rendered — an interval-only Action
	// stays a pure poller instead of also firing on click.
	Interval string
	// Reveal fires the exchange when the element scrolls into view (the
	// lazy-load pattern). Nil (the zero value) means no reveal trigger.
	// htmx renders "revealed" (the zero Reveal) or explicit intersect
	// modifiers; Datastar renders data-on-intersect with its modifier set
	// (bundle-decoded). Reveal.Exit is Datastar-only — htmx degrades to the
	// entry trigger.
	Reveal *Reveal
}

Action describes one client-initiated hypermedia exchange in transport dialects' common subset: a method on a URL, triggered by a DOM event, patching a target region.

func Delete added in v1.20.0

func Delete(url string) *Action

Delete wires a DELETE exchange.

func Get added in v1.20.0

func Get(url string) *Action

Get wires a GET exchange — the read default (fragment loads, filters, searches).

func Patch added in v1.20.0

func Patch(url string) *Action

Patch wires a PATCH exchange.

func Post added in v1.20.0

func Post(url string) *Action

Post wires a POST exchange. Mutations MUST set an explicit method — the zero-value Action renders GET in BOTH dialects.

func Put added in v1.20.0

func Put(url string) *Action

Put wires a PUT exchange.

func (Action) Attributes

func (a Action) Attributes() templ.Attributes

Attributes renders the action as templ attributes in the transport's dialect. It returns nil when the URL is empty (no wiring) so the result can always be spread into templ attribute position.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/templ-components/utils/wire"
)

func main() {
	attrs := wire.Action{Method: wire.MethodPost, URL: "/api/items", Target: "#list"}.Attributes()
	fmt.Println(attrs["hx-post"])
	fmt.Println(attrs["hx-target"])
}
Output:
/api/items
#list

func (Action) WithContentType added in v1.20.0

func (a Action) WithContentType(c ContentType) Action

WithContentType sets how field values travel (ContentTypeForm makes a Datastar action serialize the enclosing form).

func (Action) WithDebounce added in v1.20.0

func (a Action) WithDebounce(ms int) Action

WithDebounce sets the debounce window (delay:<n>ms trigger modifier under htmx, __debounce.<n>ms under Datastar). Needs an explicit event.

func (Action) WithEvent added in v1.20.0

func (a Action) WithEvent(e Event) Action

WithEvent sets the triggering DOM event. Under htmx it renders as the hx-trigger event token; under Datastar as the data-on:<event> key.

func (Action) WithFormDefaults added in v1.20.0

func (a Action) WithFormDefaults(event Event) Action

WithFormDefaults applies the host-component defaults shared by every component that wires INSIDE a form (forms.Form, FilterInput, FilterDropdown, KanbanBoard's hidden move form): an unspecified Event becomes the host's natural submit-family event, and an unspecified ContentType becomes form encoding so the form's fields travel under Datastar too. It replaces the per-package copy-and-default helpers — the copy semantics (never mutate the consumer's action) are the method's whole point.

func (Action) WithInterval added in v1.20.0

func (a Action) WithInterval(every string) Action

WithInterval sets the polling duration (ADR-0043).

func (Action) WithMethod added in v1.20.0

func (a Action) WithMethod(m Method) Action

WithMethod sets the HTTP verb explicitly (the constructors cover the common cases; this escapes for dynamic verbs).

func (Action) WithPreventDefault added in v1.20.0

func (a Action) WithPreventDefault() Action

WithPreventDefault renders the Datastar __prevent modifier (htmx ignores it — it intercepts hx-* clicks natively). See Action.PreventDefault.

func (Action) WithReveal added in v1.20.0

func (a Action) WithReveal(reveal Reveal) Action

WithReveal sets the viewport-reveal trigger (ADR-0043).

func (Action) WithSelector added in v1.20.0

func (a Action) WithSelector(selector string) Action

WithSelector selects which form serializes under Datastar ContentTypeForm ({selector: '…'}) — the runtime resolves the enclosing form via that selector instead of the action element's closest form. It NEVER targets patches: the pinned runtime reads patch targeting exclusively from the datastar-selector response header (or the fragment root's id) — use wire.Handler(PatchTarget) server-side instead.

func (Action) WithSwap added in v1.20.0

func (a Action) WithSwap(mode PatchMode) Action

WithSwap sets the region-merge style (the shared PatchMode vocabulary). Transport-asymmetric like Target: htmx renders hx-swap client-side; under Datastar the merge mode is response-driven (wire.Handler's PatchTarget.Mode) and Swap renders nothing there.

func (Action) WithTarget added in v1.20.0

func (a Action) WithTarget(target string) Action

WithTarget sets the htmx patch region (hx-target). Datastar targeting is response-driven (see PatchTarget); use WithSelector for the client-side Datastar twin.

func (Action) WithThrottle added in v1.20.0

func (a Action) WithThrottle(ms int) Action

WithThrottle sets the throttle window (throttle:<n>ms under htmx, __throttle.<n>ms under Datastar). Needs an explicit event. Throttle and debounce answer different questions — throttle bounds the RATE (fire at most once per window, trailing call last), debounce waits for QUIET — so setting both is allowed but pointless under Datastar (delay → throttle pipeline) and unsupported by htmx (delay+throttle together). Prefer one.

func (Action) WithTransport added in v1.20.0

func (a Action) WithTransport(t Transport) Action

WithTransport selects the attribute dialect (zero value stays htmx).

type ContentType added in v1.13.3

type ContentType string

ContentType selects how field values travel to the server. It mirrors the Datastar fetch action's contentType option (verified against the pinned v1.0.3 bundle — see docs/datastar-runtime-facts.md); htmx ignores it because hx-* requests serialize the enclosing form natively.

const (
	// ContentTypeUnspecified is the zero value. It resolves to ContentTypeJSON
	// (the Datastar runtime default: all signals as a JSON body).
	ContentTypeUnspecified ContentType = ""
	// ContentTypeJSON sends the Datastar signals object as a JSON body — the
	// runtime default.
	ContentTypeJSON ContentType = "json"
	// ContentTypeForm serializes the enclosing <form>'s fields: HTML5
	// constraint validation gates the request, the submitter button's
	// name/value is appended, enctype="multipart/form-data" sends a FormData
	// body (file uploads), anything else sends application/x-www-form-
	// urlencoded, and GET requests carry the fields as query parameters.
	// This is the Datastar twin of htmx's native form serialization — the
	// piece that makes whole-form submission transport-symmetric.
	ContentTypeForm ContentType = "form"
)

type Event

type Event string

Event is the DOM event that triggers the wired exchange.

const (
	// EventUnspecified is the zero value. Under htmx it is omitted so the
	// trigger engine applies its element defaults (click on buttons, submit
	// on forms, change on inputs). Under Datastar it resolves to click,
	// because data-on: requires an explicit event key.
	EventUnspecified Event = ""
	EventClick       Event = "click"
	EventSubmit      Event = "submit"
	EventChange      Event = "change"
	EventInput       Event = "input"
	EventKeyDown     Event = "keydown"
	EventKeyUp       Event = "keyup"
	EventFocus       Event = "focus"
	EventBlur        Event = "blur"
)

type Method

type Method string

Method is the HTTP method of the wired exchange.

const (
	// MethodUnspecified is the zero value. It resolves to MethodGet, the
	// safe default in both dialects.
	MethodUnspecified Method = ""
	MethodGet         Method = "get"
	MethodPost        Method = "post"
	MethodPut         Method = "put"
	MethodPatch       Method = "patch"
	MethodDelete      Method = "delete"
)

type PatchMode

type PatchMode string

PatchMode is the merge mode the Datastar runtime applies when patching a non-SSE HTML response into the region named by HeaderDatastarSelector. The value set is verified against the pinned v1.0.2 runtime bundle — see docs/datastar-runtime-facts.md.

const (
	// PatchModeUnspecified is the zero value. Handler resolves it to
	// PatchModeInner — the least destructive mode that still replaces the
	// region's content.
	PatchModeUnspecified PatchMode = ""
	PatchModeInner       PatchMode = "inner"
	PatchModeOuter       PatchMode = "outer"
	PatchModePrepend     PatchMode = "prepend"
	PatchModeAppend      PatchMode = "append"
	PatchModeBefore      PatchMode = "before"
	PatchModeAfter       PatchMode = "after"
	PatchModeReplace     PatchMode = "replace"
	// PatchModeRemove deletes the matched element(s) from the DOM instead of
	// patching content into them — the runtime's eighth mode (the pinned
	// bundle's mode set is ["remove","outer","inner","replace","prepend",
	// "append","before","after"]). Use it to retract a region, e.g. dismissing
	// a card.
	PatchModeRemove PatchMode = "remove"
)

type PatchTarget

type PatchTarget struct {
	// Selector is the CSS selector of the patch region, e.g. "#out".
	Selector string
	// Mode is the merge mode. Zero value renders as inner.
	Mode PatchMode
	// UseViewTransitions wraps this patch in the browser's View Transitions
	// API (document.startViewTransition) — the animated same-document
	// cross-fade/slide vocabulary. Response-header driven like everything
	// else: Handler stamps HeaderDatastarUseViewTransition and the runtime
	// turns it into the useViewTransition dataline (bundle-decoded from the
	// pinned v0.6.1 bundle: the header name is the kebab-cased dataline name;
	// the value coerces through a boolean check). It is patch-scoped — per
	// request, not per page — which is the deliberate contrast to htmx's
	// global htmx.ViewTransitions config flag.
	UseViewTransitions bool
}

PatchTarget describes where a Datastar caller patches a fragment response. htmx callers do not need it: hx-target already named the region client-side.

type Reveal added in v1.20.0

type Reveal struct {
	// ThresholdPercent is how much of the element must be visible before the
	// trigger fires: 0 (the default) = any part enters the viewport, 50 =
	// half visible, 100 = fully visible. Values are clamped to [0, 100].
	// Datastar renders named modifiers at the special values (50 → half,
	// 100 → full) and threshold.<n> otherwise; htmx renders
	// threshold:<fraction> only when nonzero.
	ThresholdPercent int

	// EveryEntry re-fires the trigger on EVERY viewport entry instead of the
	// zero-value fire-once behavior (htmx "revealed" = intersect once; the
	// Datastar __once modifier is omitted only when this is set).
	EveryEntry bool

	// Exit fires when the element LEAVES the viewport instead of entering.
	// Datastar renders __exit (bundle-verified). htmx has no exit semantics:
	// it degrades to the entry trigger (documented, like PatchModeReplace's
	// degradation) — a leave-only action cannot be expressed under htmx.
	Exit bool
}

Reveal configures the viewport-reveal trigger: the exchange fires when the element scrolls into view — the lazy-load / infinite-scroll pattern. The zero value is htmx's "revealed" shorthand (fire ONCE on first entry — the common lazy-load case, per the zero-value-equals-documented-default rule); set EveryEntry to re-fire on every viewport entry.

type Transport

type Transport string

Transport selects the client-side runtime that executes an Action.

const (
	// TransportUnspecified is the zero value. It resolves to TransportHTMX —
	// the library default (ADR-0030) — so plain Action literals stay ergonomic.
	TransportUnspecified Transport = ""
	// TransportHTMX wires via hx-* attributes (htmx 2.x trigger engine).
	TransportHTMX Transport = "htmx"
	// TransportDatastar wires via data-on:* attributes (@get/@post expressions).
	TransportDatastar Transport = "datastar"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL