35 |
36 |
37 | {{ end }}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/middleware.go:
--------------------------------------------------------------------------------
1 | package htmxtools
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | )
7 |
8 | // WrapFunc is middleware for inspecting http requests for htmx metadata
9 | func WrapFunc(next http.HandlerFunc) http.HandlerFunc {
10 | return func(w http.ResponseWriter, r *http.Request) {
11 | if res := ParseRequest(r); res != nil {
12 | ctx := res.ToContext(r.Context())
13 | next.ServeHTTP(w, r.WithContext(ctx))
14 | return
15 | }
16 | next.ServeHTTP(w, r)
17 | }
18 | }
19 |
20 | // Wrap is middleware for inspecting http requests for htmx metadata
21 | func Wrap(next http.Handler) http.Handler {
22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 | if res := ParseRequest(r); res != nil {
24 | ctx := res.ToContext(r.Context())
25 | next.ServeHTTP(w, r.WithContext(ctx))
26 | return
27 | }
28 | next.ServeHTTP(w, r)
29 | })
30 | }
31 |
32 | // ParseRequest parses an [http.Request] for any htmx request headers and returns an [HTMXRequest]
33 | // fields will still have to be checked for empty string at call sites
34 | func ParseRequest(r *http.Request) *HTMXRequest {
35 | tru := "true"
36 | isHTMXRequest := strings.TrimSpace(r.Header.Get(HXRequestHeader.String())) == tru
37 | if !isHTMXRequest {
38 | return nil
39 | }
40 | res := &HTMXRequest{
41 | Boosted: strings.TrimSpace(r.Header.Get(BoostedRequest.String())) == tru,
42 | CurrentURL: strings.TrimSpace(r.Header.Get(CurrentURLRequest.String())),
43 | HistoryRestore: strings.TrimSpace(r.Header.Get(HistoryRestoreRequest.String())) == tru,
44 | Prompt: strings.TrimSpace(r.Header.Get(PromptRequest.String())),
45 | Target: strings.TrimSpace(r.Header.Get(TargetRequest.String())),
46 | TriggerName: strings.TrimSpace(r.Header.Get(TriggerNameRequest.String())),
47 | Trigger: strings.TrimSpace(r.Header.Get(TriggerRequest.String())),
48 | }
49 |
50 | return res
51 | }
52 |
--------------------------------------------------------------------------------
/examples/middleware/templates/other.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
13 | Other Page
14 |
15 |
16 |
17 | {{ block "other" . }}
18 |
19 | {{ if not .HtmxRequest }}
20 |
22 | {{ else }}
23 |
24 |
25 |
26 |
Trigger:
27 |
{{ .HtmxRequest.Trigger }}
28 |
29 |
30 |
Trigger Name:
31 |
{{ .HtmxRequest.TriggerName }}
32 |
33 |
34 |
Target:
35 |
{{ .HtmxRequest.Target }}
36 |
37 |
38 |
Prompt:
39 |
{{ .HtmxRequest.Prompt }}
40 |
41 |
42 |
Current URL:
43 |
{{ .HtmxRequest.CurrentURL }}
44 |
45 |
46 |
Boosted:
47 |
{{ .HtmxRequest.Boosted }}
48 |
49 |
50 |
History Restore:
51 |
{{ .HtmxRequest.HistoryRestore }}
52 |
53 |
54 | {{ end }}
55 |
56 | {{ end }}
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/hxswap.go:
--------------------------------------------------------------------------------
1 | package htmxtools
2 |
3 | // HXSwap - https://htmx.org/attributes/hx-swap/
4 | type HXSwap int64
5 |
6 | const (
7 | // SwapUnknown is the zero value for the enum
8 | SwapUnknown HXSwap = iota
9 | // SwapInnerHTML - The default, replace the inner html of the target element - probably no reason to ever set this explicitly
10 | SwapInnerHTML
11 | // SwapOuterHTML - Replace the entire target element with the response
12 | SwapOuterHTML
13 | // SwapBeforeBegin - Insert the response before the target element
14 | SwapBeforeBegin
15 | // SwapAfterBegin - Insert the response before the first child of the target element
16 | SwapAfterBegin
17 | // SwapBeforeEnd - Insert the response after the last child of the target element
18 | SwapBeforeEnd
19 | // SwapAfterEnd - Insert the response after the target element
20 | SwapAfterEnd
21 | // SwapDelete - Deletes the target element regardless of the response
22 | SwapDelete
23 | // SwapNone - Does not append content from response (out of band items will still be processed).
24 | SwapNone
25 | )
26 |
27 | // HXSwapFromString returns an [HXSWap] from its string representation
28 | func HXSwapFromString(s string) HXSwap {
29 | switch s {
30 | case SwapInnerHTML.String():
31 | return SwapInnerHTML
32 | case SwapOuterHTML.String():
33 | return SwapOuterHTML
34 | case SwapBeforeBegin.String():
35 | return SwapBeforeBegin
36 | case SwapAfterBegin.String():
37 | return SwapAfterBegin
38 | case SwapAfterEnd.String():
39 | return SwapAfterEnd
40 | case SwapDelete.String():
41 | return SwapDelete
42 | case SwapNone.String():
43 | return SwapNone
44 | default:
45 | return SwapInnerHTML
46 | }
47 | }
48 |
49 | // String returns the string representation of a status code
50 | func (hxs HXSwap) String() string {
51 | switch hxs {
52 | case SwapInnerHTML:
53 | return "innerHTML"
54 | case SwapOuterHTML:
55 | return "outerHTML"
56 | case SwapBeforeBegin:
57 | return "beforebegin"
58 | case SwapAfterBegin:
59 | return "afterbegin"
60 | case SwapBeforeEnd:
61 | return "beforeend"
62 | case SwapAfterEnd:
63 | return "afterend"
64 | case SwapDelete:
65 | return "delete"
66 | case SwapNone:
67 | return "none"
68 | default:
69 | return "innerHTML"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/htmxresponse.go:
--------------------------------------------------------------------------------
1 | package htmxtools
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "strings"
7 | )
8 |
9 | // HTMXResponse represents the htmx elements of an [http.Response]
10 | type HTMXResponse struct {
11 | // https://htmx.org/headers/hx-location/
12 | Location string
13 | // https://htmx.org/headers/hx-push-url/
14 | PushURL string
15 | Redirect string
16 | Refresh bool
17 | // https://htmx.org/headers/hx-replace-url/
18 | ReplaceURL string
19 | // https://htmx.org/attributes/hx-swap/
20 | Reswap HXSwap
21 | // https://htmx.org/headers/hx-trigger/
22 | Trigger string
23 | // https://htmx.org/headers/hx-trigger/
24 | TriggerAfterSettle string
25 | // https://htmx.org/headers/hx-trigger/
26 | TriggerAfterSwap string
27 | }
28 |
29 | // AddToResponse adds the current state of the HTMXResponse to the http response headers
30 | func (hr *HTMXResponse) AddToResponse(w http.ResponseWriter) error {
31 | if strings.TrimSpace(hr.Location) != "" {
32 | w.Header().Set(LocationResponse.String(), hr.Location)
33 | }
34 | if strings.TrimSpace(hr.PushURL) != "" {
35 | w.Header().Set(PushURLResponse.String(), hr.PushURL)
36 | }
37 | if strings.TrimSpace(hr.Redirect) != "" {
38 | w.Header().Set(RedirectResponse.String(), hr.Redirect)
39 | }
40 | if hr.Refresh {
41 | w.Header().Set(RefreshResponse.String(), "true")
42 | }
43 | if strings.TrimSpace(hr.ReplaceURL) != "" {
44 | w.Header().Set(ReplaceURLResponse.String(), hr.ReplaceURL)
45 | }
46 | if strings.TrimSpace(hr.Trigger) != "" {
47 | w.Header().Set(TriggerResponse.String(), hr.Trigger)
48 | }
49 | if strings.TrimSpace(hr.TriggerAfterSettle) != "" {
50 | w.Header().Set(TriggerAfterSettleResponse.String(), hr.TriggerAfterSettle)
51 | }
52 | if hr.Reswap != 0 {
53 | w.Header().Set(ReswapResponse.String(), hr.Reswap.String())
54 | }
55 | if strings.TrimSpace(hr.TriggerAfterSwap) != "" {
56 | w.Header().Set(TriggerAfterSwapResponse.String(), hr.TriggerAfterSwap)
57 | }
58 | return nil
59 | }
60 |
61 | // HXLocationResponse represents the structured format of an hx-location header described here:
62 | // https://htmx.org/headers/hx-location/
63 | type HXLocationResponse struct {
64 | Path string `json:"path"`
65 | Source string `json:"source,omitempty"`
66 | Event string `json:"event,omitempty"`
67 | Handler string `json:"handler,omitempty"`
68 | Target string `json:"target,omitempty"`
69 | Swap HXSwap `json:"swap,omitempty"`
70 | Values map[string]interface{} `json:"value,omitempty"`
71 | Headers map[string]string `json:"headers,omitempty"`
72 | }
73 |
74 | // String strings
75 | func (hxl HXLocationResponse) String() string {
76 | j, _ := json.Marshal(hxl)
77 | return string(j)
78 | }
79 |
--------------------------------------------------------------------------------
/examples/middleware/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "encoding/json"
6 | "fmt"
7 | "html/template"
8 | "io/fs"
9 | "net/http"
10 | "strings"
11 |
12 | "github.com/lusis/htmxtools"
13 | "golang.org/x/exp/slog"
14 | )
15 |
16 | //go:embed templates/*
17 | var templateFS embed.FS
18 |
19 | var templates *template.Template
20 |
21 | func main() {
22 | subbed, err := fs.Sub(templateFS, "templates")
23 | if err != nil {
24 | panic(err)
25 | }
26 | tmpls, err := template.New("").ParseFS(subbed, "*")
27 | if err != nil {
28 | panic(err)
29 | }
30 | templates = tmpls
31 | mux := http.NewServeMux()
32 | mux.HandleFunc("/alert", serversideAlert)
33 | mux.HandleFunc("/button-push", buttonPush)
34 | mux.HandleFunc("/", templateMiddleware)
35 | middleware := htmxtools.WrapFunc(requestLogger(mux))
36 | if err := http.ListenAndServe(":3000", middleware); err != nil && err != http.ErrServerClosed {
37 | panic(err)
38 | }
39 | }
40 |
41 | func templateMiddleware(w http.ResponseWriter, r *http.Request) {
42 | // let's trim off the leftmost slash to find our template name
43 | p := strings.TrimLeft(r.URL.Path, "/")
44 | if p == "" {
45 | // server index by default
46 | p = "index.html"
47 | }
48 | htmxRequest := htmxtools.RequestFromContext(r.Context())
49 | data := struct {
50 | HtmxRequest *htmxtools.HTMXRequest
51 | }{
52 | HtmxRequest: htmxRequest,
53 | }
54 |
55 | if t := templates.Lookup(p); t != nil {
56 | if err := t.Execute(w, data); err != nil {
57 | slog.Error("unable to render template", "error", err)
58 | return
59 | }
60 | }
61 | }
62 |
63 | func serversideAlert(w http.ResponseWriter, r *http.Request) {
64 | if htmxRequest := htmxtools.RequestFromContext(r.Context()); htmxRequest != nil {
65 | // we're also going to ensure that this alert link doesn't make it's way into the history
66 | hxheaders := &htmxtools.HTMXResponse{
67 | ReplaceURL: htmxRequest.CurrentURL,
68 | Trigger: `{"showMessage":{"level" : "info", "message" : "this alert was trigged via the HX-Trigger header"}}`,
69 | }
70 | if err := hxheaders.AddToResponse(w); err != nil {
71 | slog.Error(err.Error())
72 | return
73 | }
74 | }
75 | }
76 |
77 | func buttonPush(w http.ResponseWriter, r *http.Request) {
78 | htmxRequest := htmxtools.RequestFromContext(r.Context())
79 | if htmxRequest != nil {
80 | j, err := json.Marshal(htmxRequest)
81 | if err != nil {
82 | w.Write([]byte("unable to encode htmxrequest to json: " + err.Error())) // nolint: errcheck
83 | }
84 | response := `
85 |
Your request:
%s
86 | `
87 | w.Write([]byte(fmt.Sprintf(response, j))) // nolint: errcheck
88 | }
89 | }
90 |
91 | func requestLogger(next http.Handler) http.HandlerFunc {
92 | return func(w http.ResponseWriter, r *http.Request) {
93 | htmxreq := htmxtools.RequestFromContext(r.Context())
94 | // log and continue
95 | slog.Info("handling request", "http.path", r.URL.Path, "http.host", r.Host, "http.method", r.Method, "http.client", r.Header.Get("User-Agent"), "content-type", r.Header.Get("content-type"), "htmxrequest", htmxreq)
96 | next.ServeHTTP(w, r)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/headers.go:
--------------------------------------------------------------------------------
1 | package htmxtools
2 |
3 | // HTMXRequestHeader is a string type
4 | type HTMXRequestHeader string
5 |
6 | // String strings
7 | func (rh HTMXRequestHeader) String() string {
8 | return string(rh)
9 | }
10 |
11 | // HTMXResponseHeader is a string type
12 | type HTMXResponseHeader string
13 |
14 | // String strings
15 | func (rh HTMXResponseHeader) String() string {
16 | return string(rh)
17 | }
18 |
19 | // https://htmx.org/reference/#headers
20 | // http headers are supposed to be case sensitive but
21 | // in case something isn't behaving somewhere, we'll use the
22 | // case from the project's page
23 | const (
24 | hxHeaderPrefix = "HX-"
25 | // Request headers
26 | // HXRequestHeader is the header that signals an htmx request. Always true if called from htmx
27 | HXRequestHeader HTMXRequestHeader = hxHeaderPrefix + "Request"
28 | // BoostedRequest indicates that the request is via an element using hx-boost
29 | BoostedRequest HTMXRequestHeader = hxHeaderPrefix + "Boosted"
30 | // CurrentURLRequest the current URL of the browser
31 | CurrentURLRequest HTMXRequestHeader = hxHeaderPrefix + "Current-URL"
32 | // HistoryRestoreRequest is true if the request is for history restoration after a miss in the local history cache
33 | HistoryRestoreRequest HTMXRequestHeader = hxHeaderPrefix + "History-Restore-Request"
34 | // PromptRequest is the user response to an hx-prompt
35 | // https://htmx.org/attributes/hx-prompt/
36 | PromptRequest HTMXRequestHeader = hxHeaderPrefix + "Prompt"
37 | // TriggerRequest is the id of the target element if it exists
38 | TriggerRequest HTMXRequestHeader = hxHeaderPrefix + "Trigger"
39 | // TriggerNameRequest is the name of the triggered element if it exists
40 | TriggerNameRequest HTMXRequestHeader = hxHeaderPrefix + "Trigger-Name"
41 | // TargetRequest is the id of the target element if it exists
42 | TargetRequest HTMXRequestHeader = hxHeaderPrefix + "Target"
43 |
44 | // Response headers
45 | // LocationResponse Allows you to do a client-side redirect that does not do a full page reload
46 | // https://htmx.org/headers/hx-location/
47 | LocationResponse HTMXResponseHeader = hxHeaderPrefix + "Location"
48 | // PushURLResponse pushes a new url into the history stack
49 | // https://htmx.org/headers/hx-push-url/
50 | PushURLResponse HTMXResponseHeader = hxHeaderPrefix + "Push-Url"
51 | // RedirectResponse can be used to do a client-side redirect to a new location
52 | RedirectResponse HTMXResponseHeader = hxHeaderPrefix + "Redirect"
53 | // RefreshResponse if set to “true” the client side will do a a full refresh of the page
54 | RefreshResponse HTMXResponseHeader = hxHeaderPrefix + "Refresh"
55 | // ReplaceURLResponse replaces the current URL in the location bar
56 | // https://htmx.org/headers/hx-replace-url/
57 | ReplaceURLResponse HTMXResponseHeader = hxHeaderPrefix + "Replace-Url"
58 | // ReswapResponse Allows you to specify how the response will be swapped. See hx-swap for possible values
59 | ReswapResponse HTMXResponseHeader = hxHeaderPrefix + "Reswap"
60 | // RetargetResponse A CSS selector that updates the target of the content update to a different element on the page
61 | RetargetResponse HTMXResponseHeader = hxHeaderPrefix + "Retarget"
62 | // TriggerResponse allows you to trigger client side events, see the documentation for more info
63 | // https://htmx.org/headers/hx-trigger/
64 | TriggerResponse HTMXResponseHeader = hxHeaderPrefix + "Trigger"
65 | // TriggerAfterSettleResponse allows you to trigger client side events, see the documentation for more info
66 | // https://htmx.org/headers/hx-trigger/
67 | TriggerAfterSettleResponse HTMXResponseHeader = hxHeaderPrefix + "Trigger-After-Settle"
68 | // TriggerAfterSwapResponse allows you to trigger client side events, see the documentation for more info
69 | // https://htmx.org/headers/hx-trigger/
70 | TriggerAfterSwapResponse HTMXResponseHeader = hxHeaderPrefix + "Trigger-After-Swap"
71 | )
72 |
--------------------------------------------------------------------------------
/middleware_test.go:
--------------------------------------------------------------------------------
1 | package htmxtools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "net/http/httptest"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func TestParseRequest(t *testing.T) {
15 | t.Parallel()
16 | type testcase struct {
17 | httpRequest *http.Request
18 | htmxRequest *HTMXRequest
19 | }
20 | validRequest, _ := http.NewRequest(http.MethodGet, "localhost", nil)
21 | validRequest.Header.Set(HXRequestHeader.String(), "true")
22 | fullRequest := validRequest.Clone(context.TODO())
23 | fullRequest.Header.Add(BoostedRequest.String(), "true")
24 | fullRequest.Header.Add(CurrentURLRequest.String(), "localhost")
25 | fullRequest.Header.Add(HistoryRestoreRequest.String(), "true")
26 | fullRequest.Header.Add(PromptRequest.String(), "did you do it?")
27 | fullRequest.Header.Add(TriggerRequest.String(), "add-thing")
28 | fullRequest.Header.Add(TargetRequest.String(), "target-div")
29 | fullRequest.Header.Add(TriggerNameRequest.String(), "thing-id")
30 | testcases := map[string]testcase{
31 | "nil": {httpRequest: &http.Request{}},
32 | "validrequest": {httpRequest: validRequest, htmxRequest: &HTMXRequest{}},
33 | "fullrequest": {httpRequest: fullRequest, htmxRequest: &HTMXRequest{
34 | Boosted: true,
35 | CurrentURL: "localhost",
36 | HistoryRestore: true,
37 | Prompt: "did you do it?",
38 | Target: "target-div",
39 | Trigger: "add-thing",
40 | TriggerName: "thing-id",
41 | }},
42 | }
43 | for n, tc := range testcases {
44 | t.Run(n, func(t *testing.T) {
45 | res := ParseRequest(tc.httpRequest)
46 | if tc.htmxRequest == nil {
47 | require.Nil(t, res)
48 | } else {
49 | require.NotNil(t, res)
50 | require.Equal(t, tc.htmxRequest.Boosted, res.Boosted, "boosted should match")
51 | require.Equal(t, tc.htmxRequest.CurrentURL, res.CurrentURL, "current url should match")
52 | require.Equal(t, tc.htmxRequest.HistoryRestore, res.HistoryRestore, "history restore should match")
53 | require.Equal(t, tc.htmxRequest.Prompt, res.Prompt, "prompt should match")
54 | require.Equal(t, tc.htmxRequest.Target, res.Target, "target should match")
55 | require.Equal(t, tc.htmxRequest.Trigger, res.Trigger, "trigger should match")
56 | require.Equal(t, tc.htmxRequest.TriggerName, res.TriggerName, "trigger name should match")
57 | }
58 | })
59 | }
60 | }
61 |
62 | func TestMiddleware(t *testing.T) {
63 | t.Parallel()
64 | htmxReq := &HTMXRequest{
65 | Boosted: true,
66 | CurrentURL: "localhost",
67 | HistoryRestore: true,
68 | Prompt: "did you do it?",
69 | Target: "target-div",
70 | Trigger: "add-thing",
71 | TriggerName: "thing-id",
72 | }
73 | var extractedRequest *HTMXRequest
74 | // need a func to wrap with the middleware and capture the context details
75 | testHandlerFunc := func(w http.ResponseWriter, r *http.Request) {
76 | extractedRequest = RequestFromContext(r.Context())
77 | }
78 | server := httptest.NewServer(WrapFunc(testHandlerFunc))
79 | req, err := http.NewRequest(http.MethodGet, server.URL, nil)
80 | require.NoError(t, err, "new request should not error")
81 | require.NotNil(t, req, "http request should not be nil")
82 | // do the request without the headers first
83 | nres, nerr := http.DefaultClient.Do(req)
84 | require.NoError(t, nerr, "request should not error")
85 | require.NotNil(t, nres, "result should not be nil")
86 | require.Nil(t, extractedRequest, "extracted result should be nil")
87 | // add the htmx header
88 | req.Header.Set(HXRequestHeader.String(), "true")
89 | req.Header.Add(BoostedRequest.String(), "true")
90 | req.Header.Add(CurrentURLRequest.String(), "localhost")
91 | req.Header.Add(HistoryRestoreRequest.String(), "true")
92 | req.Header.Add(PromptRequest.String(), "did you do it?")
93 | req.Header.Add(TriggerRequest.String(), "add-thing")
94 | req.Header.Add(TargetRequest.String(), "target-div")
95 | req.Header.Add(TriggerNameRequest.String(), "thing-id")
96 | res, err := http.DefaultClient.Do(req)
97 | require.NoError(t, err, "making request should not error")
98 | require.NotNil(t, res, "http request result should not be nil")
99 | require.NotNil(t, extractedRequest, "request from context should not be nil")
100 | require.Equal(t, htmxReq.Boosted, extractedRequest.Boosted)
101 | }
102 |
103 | // addHeaders adds hmtx headers to the provided http request for testing
104 | func (hr *HTMXResponse) addHeaders(r *http.Request, headers ...map[HTMXRequestHeader]string) error { // nolint: unused
105 | for _, h := range headers {
106 | for k, v := range h {
107 | if strings.TrimSpace(r.Header.Get(k.String())) != "" {
108 | return fmt.Errorf("header already set: %s: %s", k, v)
109 | }
110 | r.Header.Set(k.String(), v)
111 | }
112 | }
113 | return nil
114 | }
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # htmxtools
2 | `htmxtools` is a collection of constants and utilities for working with [htmx](https://htmx.org) from Go.
3 |
4 | `htmx` plays REALLY nice with Go and (somewhat less nice but still nice) Go templates.
5 |
6 |
7 | ## General usage
8 | There are different things in here for different use cases - http middleware as well as some constants and helpers for htmx requests
9 |
10 |
11 | ### Constants and such
12 | There are quite a few constants and enums for some various bits of htmx that can help cut down on some error prone duplication.
13 |
14 | #### Response Headers
15 | The response headers can be used as described here: https://htmx.org/reference/#response_headers and they are one of the coolest things about htmx.
16 |
17 | You can do things like ensure the browser url bar and history point to a real html page (as opposed to a fragment which is the default when using hx-get and the like).
18 |
19 | I use this exact pattern in another project when an add or delete call is made to the backend (inside my handler):
20 | ```go
21 | w.Header().Add(htmxtools.LocationResponse.String(), `{"path":"delete-status-fragment", "target":"#content-div"}`)
22 | w.Header().Add(htmxtools.ReplaceURLResponse.String(), "status.html")
23 | w.WriteHeader(http.StatusAccepted)
24 | ```
25 | When the handler is called via an `hx-` request, the contents `delete-status-fragment` will replace the contents of the div with id `content-div`.
26 | However, unlike the default behaviour, the url bar will show `status.html` and be safe to reload while the default would show a path of `delete-status-fragment` which is not a valid full html page
27 |
28 | _if you feel like the headers aren't working, make sure you actually wrote them to the http response. I make this mistake ALL THE TIME_
29 |
30 | There's also a helper if you want for building the headers in a safer way:
31 |
32 | ```go
33 | if htmxRequest := htmxtools.RequestFromContext(r.Context()); htmxRequest != nil {
34 | hxheaders := &htmxtools.HTMXResponse{
35 | ReplaceURL: htmxRequest.CurrentURL,
36 | Reswap: htmxtools.SwapOuterHTML,
37 | }
38 | if err := hxheaders.AddToResponse(w); err != nil {
39 | return
40 | }
41 | }
42 | ```
43 |
44 | ### Middleware
45 | ```go
46 | http.Handle("/", htmxtools.Wrap(myhandler))
47 | ```
48 | or
49 | ```go
50 | http.Handle("/",htmxtools.WrapFunc(myhandlerfunc))
51 | ```
52 |
53 | This will detect htmx requests and inject the details into the context.
54 | You can extract the details down the line via:
55 | ```go
56 | func somefunc(w http.ResponseWriter, r *http.Request) {
57 | htmxrequest := htmxtools.RequestFromContext(r.Context())
58 | if htmxrequest == nil {
59 | // do something for non-htmx requests
60 | } else {
61 | // do something for htmx requests
62 | // note that not all fields will be populated so you'll want to check
63 | }
64 | }
65 | ```
66 |
67 | ## Examples
68 | The following contains a few different ways to use this library
69 |
70 | ### in-repo example
71 | In the `examples/middleware` directory there's a small example:
72 | ```
73 | go run examples/middleware/main.go
74 | ```
75 |
76 | will start a small webserver on http://localhost:3000
77 |
78 | - Loading the page will present a button, that when clicked, makes an htmx request to the backend which returns details about the htmx request:
79 |
80 | 
81 |
82 | - Clicking "drink me" will ask for input and render a response from the server:
83 |
84 | 
85 | visiting http://localhost:3000/other.html will show a similar page which doesn't go to the backend for data but passes through the middleware which injects the htmx request details in the template
86 |
87 | - Clicking "eat me" will respond with a server-side generated htmx alert via headers:
88 | 
89 |
90 | - Visting http://localhost:3000/other.html , will render a page that operates entirely client side (with the exception of the template values that are injected by the middleware)
91 |
92 | 
93 |
94 | - clicking the button will generate an htmx request for the same file (with `hx-confirm` client side) but will be passed an htmx request struct via the template execution:
95 |
96 | 
97 | 
98 |
99 | ### Building your own handler
100 | If you wanted to, you can use the various bits to build your handler the way you want:
101 |
102 | ```go
103 | // responds to htmx requests with the provided template updating the provided target
104 | // replace is the path to replace in the url bar
105 | func hxOnlyHandler(template, target string, next http.Handler, replace string) http.HandlerFunc {
106 | return func(w http.ResponseWriter, r *http.Request) {
107 | htmxrequest := htmxtools.RequestFromContext(r.Context())
108 | if htmxrequest == nil {
109 | next.ServeHTTP(w, r)
110 | return
111 | }
112 | htmxloc := fmt.Sprintf(`{"path":"%s","target":"%s"}`, template, target)
113 | w.Header().Add(htmxtools.LocationResponse.String(), htmxloc)
114 | if strings.TrimSpace(replace) != "" {
115 | w.Header().Add(htmxtools.ReplaceURLResponse.String(), replace)
116 | }
117 | w.WriteHeader(http.StatusAccepted)
118 | }
119 | }
120 | ```
121 |
122 | ## Templating tips
123 | I am not a go template expert. I've tended to avoid them in the past because of the runtime implications however now that I've been working on browser content, I had to really dive back in.
124 |
125 | ### Use blocks
126 | Blocks are a REALLY REALLY nice thing in go templates. They allow you to define a template inside of another templare. Note that blocks are still rendered (unless something inside the block says not to).
127 |
128 | *tl;dr: wrap chunks of html content that you might want to reuse via htmx in a ```{{ block "unique-block-name" .}}blahblah{{end}}```* and call them by block name via `hx-get` (note that this requires your server to understand serving templates - example below)
129 |
130 | Take the following template (`index.html`) from this repo:
131 | ```html
132 |
133 |
134 | {{ block "head" . }}
135 |
136 |
137 |
138 |
141 |
144 | Middleware Demo
145 |
146 | {{ end }}
147 |
148 | {{ block "body" . }}
149 |
150 |
158 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | {{ end }}
167 |
168 | ```
169 |
170 | This is our index page and can be rendered as a whole html page as is.
171 | Now let's take a look at another file in the same directory:
172 |
173 | ```html
174 |
175 |
176 |
177 |
178 |
179 |
180 |
183 |
186 | Other Page
187 |
188 |
189 |
190 | {{ block "other" . }}
191 |
192 | {{ if not .HtmxRequest }}
193 |
194 | {{ else }}
195 |
196 |
197 | {{ end }}
198 |
199 | {{ end }}
200 |
201 |
202 | ```
203 |
204 | If we wanted to, we could rewrite the above template like so:
205 | ```html
206 |
207 |
208 |
209 | {{ template "head" . }}
210 |
211 |
212 | {{ block "other" . }}
213 |
214 | {{ if not .HtmxRequest }}
215 |
216 | {{ else }}
217 |
218 |
219 | {{ end }}
220 |
221 | {{ end }}
222 |
223 |
224 | ```
225 |
226 | This is non-htmx specific mind you but it DOES open up some fun tricks with htmx. Imagine we have the following html
227 |
228 | ```html
229 |
230 |
231 |
232 |
233 |
234 |
235 |
238 |
241 | Other Page
242 |
243 |
244 |
245 | {{ block "list-items" . }}
246 |
247 |
248 |
249 | ```
250 |
251 | Now let's say we want the `list-items` html to be rendered somewhere else. We can do this now:
252 |
253 | ```html
254 |
255 |
256 |
257 |
258 |
259 |
260 |
263 |
266 | Third Page
267 |
268 |
269 |
270 |
271 |
272 |
273 | ```
274 |
275 | The above div (`other-list-items`) will be replaced with the contents of the `list-items` block from above.
276 |
277 | Note that doing so will, by default, update the url and location history to contain `http://hostname/list-items` which is NOT a valid html page and would just render the table only. To work around this, set the `HX-Replace-Url` header to set to a valid page or `false`. You can also set the attribute on the element as well via [`hx-replace-url`](https://htmx.org/attributes/hx-replace-url/)
278 |
279 |
280 | ## TODO
281 |
282 | - add structs for htmx json (i.e. json passed to `HX-Location`)
--------------------------------------------------------------------------------