htmx-go is a type-safe library for working with HTMX in Go.
Less time fiddling with HTTP headers, more time developing awesome Hypermedia-driven applications.
Check if requests are from HTMX, and use a type-safe, declarative syntax for HTMX response headers to control HTMX behavior from the server.
Write triggers for client-side events effectively without dealing with JSON serialization. With this approach, event-driven applications are easier to develop.
Use Swap Strategy methods to fine-tune hx-swap
behavior.
Uses standard net/http
types.
Has basic integration with templ components.
import (
"net/http"
"github.com/angelofallars/htmx-go"
)
func handler(w http.ResponseWriter, r *http.Request) {
if htmx.IsHTMX(r) {
htmx.NewResponse().
Reswap(htmx.SwapBeforeEnd).
Retarget("#contacts").
AddTrigger(htmx.Trigger("enable-submit")).
AddTrigger(htmx.TriggerDetail("display-message", "Hello world!")).
Write(w)
}
}
Think this project is awesome? Consider sponsoring me đź’™
Use go get.
go get github.com/angelofallars/htmx-go
Then import htmx-go:
import "github.com/angelofallars/htmx-go"
You can determine if a request is from HTMX. With this, you can add custom handling for non-HTMX requests.
You can also use this for checking if this is a GET request for the initial page load on your website, as initial page load requests don't use HTMX.
func handler(w http.ResponseWriter, r *http.Request) {
if htmx.IsHTMX(r) {
// logic for handling HTMX requests
} else {
// logic for handling non-HTMX requests (e.g. render a full page for first-time visitors)
}
}
func handler(w http.ResponseWriter, r *http.Request) {
if htmx.IsBoosted(r) {
// logic for handling boosted requests
} else {
// logic for handling non-boosted requests
}
}
htmx-go takes inspiration from Lip Gloss for a declarative way of specifying HTMX response headers.
Make a response writer with htmx.NewResponse()
, and add a
header to it to make the page refresh:
func handler(w http.ResponseWriter, r *http.Request) {
writer := htmx.NewResponse().Refresh(true)
writer.Write(w)
}
func handler(w http.ResponseWriter, r *http.Request) {
htmx.NewResponse().
// Override 'hx-target' to specify which target to load into
Retarget("#errors").
// Also override the 'hx-swap' value of the request
Reswap(htmx.SwapBeforeEnd).
Write(w)
}
You can add triggers to trigger client-side events. htmx-go takes care of formatting and JSON serialization of the header values.
Define event triggers:
htmx.Trigger(eventName string)
- A trigger with no details.htmx.TriggerDetail(eventName string, detailValue string)
- A trigger with one detail value.htmx.TriggerObject(eventName string, detailObject any)
- A trigger with a JSON-serializable detail object. Recommended to pass in eithermap[string]string
or structs with JSON field tags.
Set trigger headers using the preceding triggers:
Response.AddTrigger(trigger ...EventTrigger)
- appends to theHX-Trigger
headerResponse.AddTriggerAfterSettle(trigger ...EventTrigger)
- appends to theHX-Trigger-After-Settle
headerResponse.AddTriggerAfterSwap(trigger ...EventTrigger)
- appends to theHX-Trigger-After-Swap
header
htmx.NewResponse().
AddTrigger(htmx.Trigger("myEvent"))
// HX-Trigger: myEvent
htmx.NewResponse().
AddTrigger(htmx.TriggerDetail("showMessage", "Here Is A Message"))
// HX-Trigger: {"showMessage":"Here Is A Message"}
htmx.NewResponse().
AddTrigger(
htmx.TriggerDetail("hello", "world"),
htmx.TriggerObject("myEvent", map[string]string{
"level": "info",
"message": "Here Is A Message",
}),
)
// HX-Trigger: {"hello":"world","myEvent":{"level":"info","message":"Here is a Message"}}
Tip
Alpine.js and Hyperscript can listen to and receive details from events triggered by htmx-go. This makes triggers initiated by the server very handy for event-driven applications!
For Alpine.js, you can register an x-on:<EventName>.window
listener. The .window
modifier
is important because HTMX dispatches events from the root window
object.
To receive values sent by htmx.TriggerDetail
and htmx.TriggerObject
,
you can use $event.detail.value
.
Response.Reswap()
takes in SwapStrategy
values from this library.
htmx.NewResponse().
Reswap(htmx.SwapInnerHTML)
// HX-Reswap: innerHTML
htmx.NewResponse().
Reswap(htmx.SwapAfterEnd.Transition(true))
// HX-Reswap: innerHTML transition:true
Exported SwapStrategy
constant values can be appended with modifiers through their methods.
If successive methods write to the same modifier,
the modifier is always replaced with the latest one.
import "time"
htmx.SwapInnerHTMl.After(time.Second * 1)
// HX-Reswap: innerHTML swap:1s
htmx.SwapBeforeEnd.Scroll(htmx.Bottom)
// HX-Reswap: beforeend scroll:bottom
htmx.SwapAfterEnd.IgnoreTitle(true)
// HX-Reswap: afterend ignoreTitle:true
htmx.SwapAfterEnd.FocusScroll(true)
// HX-Reswap: afterend ignoreTitle:true
htmx.SwapInnerHTML.ShowOn("#another-div", htmx.Top)
// HX-Reswap: innerHTML show:#another-div:top
// Modifier chaining
htmx.SwapInnerHTML.ShowOn("#another-div", htmx.Top).After(time.Millisecond * 500)
// HX-Reswap: innerHTML show:#another-div:top swap:500ms
htmx.SwapBeforeBegin.ShowWindow(htmx.Top)
// HX-Reswap: beforebegin show:window:top
htmx.SwapDefault.ShowNone()
// HX-Reswap: show:none
HTMX response writers can be declared outside of functions with var
so you can reuse them in several
places.
Caution
If you're adding additional headers to a global response writer, always use the .Clone()
method
to avoid accidentally modifying the global response writer.
var deleter = htmx.NewResponse().
Reswap(htmx.SwapDelete)
func(w http.ResponseWriter, r *http.Request) {
deleter.Clone().
Reselect("#messages").
Write(w)
}
HTMX pairs well with Templ, and this library is no exception.
You can render both the necessary HTMX response headers and Templ components in
one step with the .RenderTempl()
method.
// hello.templ
templ Hello() {
<div>Hello { name }!</div>
}
// main.go
func(w http.ResponseWriter, r *http.Request) {
htmx.NewResponse().
Retarget("#hello").
RenderTempl(r.Context(), w, Hello())
}
Note
To avoid issues with custom HTTP status code headers with this approach,
it's recommended to use Response().StatusCode()
so the status code header
is always set after the HTMX headers.
If you have an element that is polling a URL and you want it to stop, use the
htmx.StatusStopPolling
286 status code in a response to cancel the polling. HTMX documentation
reference
w.WriteHeader(htmx.StatusStopPolling)
If you need to work with HTMX headers directly, htmx-go provides constant values for all HTTP header field names of HTMX so you don't have to write them yourself. This mitigates the risk of writing header names with typos.
// Request headers
const (
HeaderBoosted = "HX-Boosted"
HeaderCurrentURL = "HX-Current-URL"
HeaderHistoryRestoreRequest = "HX-History-Restore-Request"
HeaderPrompt = "HX-Prompt"
HeaderRequest = "HX-Request"
HeaderTarget = "HX-Target"
HeaderTriggerName = "Hx-Trigger-Name"
)
// Common headers
const (
HeaderTrigger = "HX-Trigger"
)
// Response headers
const (
HeaderLocation = "HX-Location"
HeaderPushURL = "HX-Push-Url"
HeaderRedirect = "HX-Redirect"
HeaderRefresh = "HX-Refresh"
HeaderReplaceUrl = "HX-Replace-Url"
HeaderReswap = "HX-Reswap"
HeaderRetarget = "HX-Retarget"
HeaderReselect = "HX-Reselect"
HeaderTriggerAfterSettle = "HX-Trigger-After-Settle"
HeaderTriggerAfterSwap = "HX-Trigger-After-Swap"
)
This library is compatible with the standard net/http
library, as well as other routers like Chi
and Gorilla Mux that use the standard http.HandlerFunc
handler type.
With the Echo web framework, try passing in context.Request()
and
context.Response().Writer
for requests and responses, respectively.
With the Gin web framework on the other hand, try using context.Request
and
context.Writer
.
If you use Fiber, it is recommended to use htmx-fiber
instead, which is a fork of htmx-go.
Pull requests are welcome!