base on </> Build awesome HTMX + Go projects faster. # <img src="https://github.com/angelofallars/htmx-go/assets/39676098/c1a14954-27fd-4276-8948-0800e5372b14" width="400px"> [![GoDoc](https://pkg.go.dev/badge/github.com/angelofallars/htmx-go?status.svg)](https://pkg.go.dev/github.com/angelofallars/htmx-go?tab=doc) [![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/angelofallars/htmx-go/go.yml?cacheSeconds=30)](https://github.com/angelofallars/htmx-go/actions) [![License](https://img.shields.io/github/license/angelofallars/htmx-go)](./LICENSE) [![Stars](https://img.shields.io/github/stars/angelofallars/htmx-go)](https://github.com/angelofallars/htmx-go/stargazers) [![Discord](https://img.shields.io/discord/725789699527933952?label=htmx%20discord)](https://htmx.org/discord) htmx-go is a **type-safe** library for working with [HTMX](https://htmx.org/) 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](#triggers) for client-side events effectively without dealing with JSON serialization. With this approach, **event-driven** applications are easier to develop. Use [Swap Strategy](#swap-strategy) methods to fine-tune `hx-swap` behavior. Uses standard `net/http` types. Has basic [integration](#templ-integration) with [templ](https://templ.guide/) components. ```go 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](https://github.com/sponsors/angelofallars) 💙 ## Installation Use go get. ```sh go get github.com/angelofallars/htmx-go ``` Then import htmx-go: ```go import "github.com/angelofallars/htmx-go" ``` ## HTMX Requests ### Check request origin 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. ```go 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) } } ``` ### Check if request is Boosted (`hx-boost`) ```go 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 responses htmx-go takes inspiration from [Lip Gloss](https://github.com/charmbracelet/lipgloss) for a declarative way of specifying HTMX response headers. ### Basic usage Make a response writer with `htmx.NewResponse()`, and add a header to it to make the page refresh: ``` go func handler(w http.ResponseWriter, r *http.Request) { writer := htmx.NewResponse().Refresh(true) writer.Write(w) } ``` ### Retarget response to a different element ```go 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) } ``` ### Triggers [HTMX Reference: `hx-trigger`](https://htmx.org/headers/hx-trigger/) You can add triggers to trigger client-side [events](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/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 either `map[string]string` or structs with JSON field tags. Set trigger headers using the preceding triggers: - `Response.AddTrigger(trigger ...EventTrigger)` - appends to the `HX-Trigger` header - `Response.AddTriggerAfterSettle(trigger ...EventTrigger)` - appends to the `HX-Trigger-After-Settle` header - `Response.AddTriggerAfterSwap(trigger ...EventTrigger)` - appends to the `HX-Trigger-After-Swap` header ```go 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](https://alpinejs.dev/) and [Hyperscript](https://hyperscript.org) 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`. ### Swap strategy [HTMX Reference: `hx-swap`](https://htmx.org/attributes/hx-swap/) `Response.Reswap()` takes in `SwapStrategy` values from this library. ```go 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. ```go 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 ``` ### Code organization 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. ```go var deleter = htmx.NewResponse(). Reswap(htmx.SwapDelete) func(w http.ResponseWriter, r *http.Request) { deleter.Clone(). Reselect("#messages"). Write(w) } ``` ### Templ integration HTMX pairs well with [Templ](https://templ.guide), 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. ```go // 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. ### Stop polling 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](https://htmx.org/docs/#polling) ```go w.WriteHeader(htmx.StatusStopPolling) ``` ## Header names 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. ```go // 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" ) ``` ## Compatibility 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`](https://github.com/sopa0/htmx-fiber) instead, which is a fork of htmx-go. ## Additional resources - [HTMX - HTTP Header Reference](https://htmx.org/reference/#headers) ## Contributing Pull requests are welcome! ## License [MIT](./LICENSE) ", Assign "at most 3 tags" to the expected json: {"id":"5422","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"