htmxtools

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Jun 1, 2023 License: MIT Imports: 4 Imported by: 0

README

htmxtools

htmxtools is a collection of constants and utilities for working with htmx from Go.

htmx plays REALLY nice with Go and (somewhat less nice but still nice) Go templates.

General usage

There are different things in here for different use cases - http middleware as well as some constants and helpers for htmx requests

Constants and such

There are quite a few constants and enums for some various bits of htmx that can help cut down on some error prone duplication.

Response Headers

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.

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).

I use this exact pattern in another project when an add or delete call is made to the backend (inside my handler):

w.Header().Add(htmxtools.LocationResponse.String(), `{"path":"delete-status-fragment", "target":"#content-div"}`)
w.Header().Add(htmxtools.ReplaceURLResponse.String(), "status.html")
w.WriteHeader(http.StatusAccepted)

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. 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

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

There's also a helper if you want for building the headers in a safer way:

if htmxRequest := htmxtools.RequestFromContext(r.Context()); htmxRequest != nil {
        hxheaders := &htmxtools.HTMXResponse{
			ReplaceURL: htmxRequest.CurrentURL,
            Reswap: htmxtools.SwapOuterHTML,
		}
		if err := hxheaders.AddToResponse(w); err != nil {
			return
		}
}
Middleware
http.Handle("/", htmxtools.Wrap(myhandler))

or

http.Handle("/",htmxtools.WrapFunc(myhandlerfunc))

This will detect htmx requests and inject the details into the context. You can extract the details down the line via:

func somefunc(w http.ResponseWriter, r *http.Request) {
    htmxrequest := htmxtools.RequestFromContext(r.Context())
    if htmxrequest == nil {
        // do something for non-htmx requests
    } else {
        // do something for htmx requests
        // note that not all fields will be populated so you'll want to check
    }
}

Examples

The following contains a few different ways to use this library

in-repo example

In the examples/middleware directory there's a small example:

go run examples/middleware/main.go

will start a small webserver on http://localhost:3000

  • Loading the page will present a button, that when clicked, makes an htmx request to the backend which returns details about the htmx request:

  • Clicking "drink me" will ask for input and render a response from the server:

drink me button dialogdrink me button response from server 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

  • Clicking "eat me" will respond with a server-side generated htmx alert via headers: browser alert dialog

  • 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)

  • 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:

initial other page with confirmation dialog presented initial other page post confirm with an html table showing the htmx request data

Building your own handler

If you wanted to, you can use the various bits to build your handler the way you want:

// responds to htmx requests with the provided template updating the provided target
// replace is the path to replace in the url bar
func hxOnlyHandler(template, target string, next http.Handler, replace string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        htmxrequest := htmxtools.RequestFromContext(r.Context())
        if htmxrequest == nil {
            next.ServeHTTP(w, r)
            return
        }
        htmxloc := fmt.Sprintf(`{"path":"%s","target":"%s"}`, template, target)
        w.Header().Add(htmxtools.LocationResponse.String(), htmxloc)
        if strings.TrimSpace(replace) != "" {
            w.Header().Add(htmxtools.ReplaceURLResponse.String(), replace)
        }
        w.WriteHeader(http.StatusAccepted)
    }
}

Templating tips

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.

Use blocks

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).

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)

Take the following template (index.html) from this repo:

<!DOCTYPE html>
<html lang="en">
{{ block "head" . }}
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/[email protected]"
        integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"
        crossorigin="anonymous"></script>
    <script>
        htmx.logAll();
    </script>
    <title>Middleware Demo</title>
</head>
{{ end }}

{{ block "body" . }}
<body>
    <script type="text/javascript">
        // for our post-swap alert
        document.body.addEventListener("showMessage", function (evt) {
            if (evt.detail.level === "info") {
                alert(evt.detail.message);
            }
        });
    </script>
    <div id="button-div"><button id="drink-me-button" hx-get="button-push" hx-target="#body-content"
            name="drink-me-button-name" hx-prompt="are you sure? type something to confirm">Drink me!</button>
    </div>
    <!-- set hx-swap to none so our button doesn't get replaced with empty results -->
    <div id="server-side-alert"><button id="server-side-alert" hx-get="alert" hx-swap="none">Eat me!</button>
    </div>
    <div id="body-content"></div>
</body>
{{ end }}
</html>

This is our index page and can be rendered as a whole html page as is. Now let's take a look at another file in the same directory:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/[email protected]"
        integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"
        crossorigin="anonymous"></script>
    <script>
        htmx.logAll();
    </script>
    <title>Other Page</title>
</head>

<body>
    {{ block "other" . }}
    <div id="other-content">
        {{ if not .HtmxRequest }}
        <!-- content when not htmx -->
        {{ else }}
        <!-- we're passing in the htmxrequest struct we got from the context so we can refer to its values here-->
        <!-- other content here-->
        {{ end }}
    </div>
    {{ end }}
</body>
</html>

If we wanted to, we could rewrite the above template like so:

<!DOCTYPE html>
<html lang="en">

{{ template "head" . }}

<body>
    {{ block "other" . }}
    <div id="other-content">
        {{ if not .HtmxRequest }}
        <!-- content when not htmx -->
        {{ else }}
        <!-- we're passing in the htmxrequest struct we got from the context so we can refer to its values here-->
        <!-- other content here-->
        {{ end }}
    </div>
    {{ end }}
</body>
</html>

This is non-htmx specific mind you but it DOES open up some fun tricks with htmx. Imagine we have the following html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/[email protected]"
        integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"
        crossorigin="anonymous"></script>
    <script>
        htmx.logAll();
    </script>
    <title>Other Page</title>
</head>

<body>
    {{ block "list-items" . }}
    <table><!-- insert your table rows and such here - maybe with template placeholdrs --></table>
</body>
</html>

Now let's say we want the list-items html to be rendered somewhere else. We can do this now:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/[email protected]"
        integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"
        crossorigin="anonymous"></script>
    <script>
        htmx.logAll();
    </script>
    <title>Third Page</title>
</head>

<body>
    <div id="other-list-items" hx-get="list-items"></div>
</body>
</html>

The above div (other-list-items) will be replaced with the contents of the list-items block from above.

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

TODO

  • add structs for htmx json (i.e. json passed to HX-Location)

Documentation

Overview

Package htmxtools contains helpers and such for working with htmx from go

Index

Constants

View Source
const (

	// Request headers
	// HXRequestHeader is the header that signals an htmx request. Always true if called from htmx
	HXRequestHeader HTMXRequestHeader = hxHeaderPrefix + "Request"
	// BoostedRequest indicates that the request is via an element using hx-boost
	BoostedRequest HTMXRequestHeader = hxHeaderPrefix + "Boosted"
	// CurrentURLRequest the current URL of the browser
	CurrentURLRequest HTMXRequestHeader = hxHeaderPrefix + "Current-URL"
	// HistoryRestoreRequest is true if the request is for history restoration after a miss in the local history cache
	HistoryRestoreRequest HTMXRequestHeader = hxHeaderPrefix + "History-Restore-Request"
	// PromptRequest is the user response to an hx-prompt
	// https://htmx.org/attributes/hx-prompt/
	PromptRequest HTMXRequestHeader = hxHeaderPrefix + "Prompt"
	// TriggerRequest is the id of the target element if it exists
	TriggerRequest HTMXRequestHeader = hxHeaderPrefix + "Trigger"
	// TriggerNameRequest is the name of the triggered element if it exists
	TriggerNameRequest HTMXRequestHeader = hxHeaderPrefix + "Trigger-Name"
	// TargetRequest is the id of the target element if it exists
	TargetRequest HTMXRequestHeader = hxHeaderPrefix + "Target"

	// Response headers
	// LocationResponse Allows you to do a client-side redirect that does not do a full page reload
	// https://htmx.org/headers/hx-location/
	LocationResponse HTMXResponseHeader = hxHeaderPrefix + "Location"
	// PushURLResponse pushes a new url into the history stack
	// https://htmx.org/headers/hx-push-url/
	PushURLResponse HTMXResponseHeader = hxHeaderPrefix + "Push-Url"
	// RedirectResponse can be used to do a client-side redirect to a new location
	RedirectResponse HTMXResponseHeader = hxHeaderPrefix + "Redirect"
	// RefreshResponse if set to “true” the client side will do a a full refresh of the page
	RefreshResponse HTMXResponseHeader = hxHeaderPrefix + "Refresh"
	// ReplaceURLResponse replaces the current URL in the location bar
	// https://htmx.org/headers/hx-replace-url/
	ReplaceURLResponse HTMXResponseHeader = hxHeaderPrefix + "Replace-Url"
	// ReswapResponse Allows you to specify how the response will be swapped. See hx-swap for possible values
	ReswapResponse HTMXResponseHeader = hxHeaderPrefix + "Reswap"
	// RetargetResponse A CSS selector that updates the target of the content update to a different element on the page
	RetargetResponse HTMXResponseHeader = hxHeaderPrefix + "Retarget"
	// TriggerResponse allows you to trigger client side events, see the documentation for more info
	// https://htmx.org/headers/hx-trigger/
	TriggerResponse HTMXResponseHeader = hxHeaderPrefix + "Trigger"
	// TriggerAfterSettleResponse allows you to trigger client side events, see the documentation for more info
	// https://htmx.org/headers/hx-trigger/
	TriggerAfterSettleResponse HTMXResponseHeader = hxHeaderPrefix + "Trigger-After-Settle"
	// TriggerAfterSwapResponse allows you to trigger client side events, see the documentation for more info
	// https://htmx.org/headers/hx-trigger/
	TriggerAfterSwapResponse HTMXResponseHeader = hxHeaderPrefix + "Trigger-After-Swap"
)

https://htmx.org/reference/#headers http headers are supposed to be case sensitive but in case something isn't behaving somewhere, we'll use the case from the project's page

Variables

This section is empty.

Functions

func Wrap

func Wrap(next http.Handler) http.Handler

Wrap is middleware for inspecting http requests for htmx metadata

func WrapFunc

func WrapFunc(next http.HandlerFunc) http.HandlerFunc

WrapFunc is middleware for inspecting http requests for htmx metadata

Types

type HTMXRequest

type HTMXRequest struct {
	// https://htmx.org/attributes/hx-boost/
	Boosted        bool
	CurrentURL     string
	HistoryRestore bool
	// https://htmx.org/attributes/hx-prompt/
	Prompt      string
	Target      string
	TriggerName string
	Trigger     string
}

HTMXRequest represents the htmx elements of an http.Request fields may be empty strings

func ParseRequest

func ParseRequest(r *http.Request) *HTMXRequest

ParseRequest parses an http.Request for any htmx request headers and returns an HTMXRequest fields will still have to be checked for empty string at call sites

func RequestFromContext

func RequestFromContext(ctx context.Context) *HTMXRequest

RequestFromContext parses the htmx request from the provided context

func (*HTMXRequest) ToContext

func (hr *HTMXRequest) ToContext(ctx context.Context) context.Context

ToContext adds the HTMXRequest details to the provided parent context

type HTMXRequestHeader

type HTMXRequestHeader string

HTMXRequestHeader is a string type

func (HTMXRequestHeader) String

func (rh HTMXRequestHeader) String() string

String strings

type HTMXResponse

HTMXResponse represents the htmx elements of an http.Response

func (*HTMXResponse) AddToResponse

func (hr *HTMXResponse) AddToResponse(w http.ResponseWriter) error

AddToResponse adds the current state of the HTMXResponse to the http response headers

type HTMXResponseHeader

type HTMXResponseHeader string

HTMXResponseHeader is a string type

func (HTMXResponseHeader) String

func (rh HTMXResponseHeader) String() string

String strings

type HXLocationResponse

type HXLocationResponse struct {
	Path    string                 `json:"path"`
	Source  string                 `json:"source,omitempty"`
	Event   string                 `json:"event,omitempty"`
	Handler string                 `json:"handler,omitempty"`
	Target  string                 `json:"target,omitempty"`
	Swap    HXSwap                 `json:"swap,omitempty"`
	Values  map[string]interface{} `json:"value,omitempty"`
	Headers map[string]string      `json:"headers,omitempty"`
}

HXLocationResponse represents the structured format of an hx-location header described here: https://htmx.org/headers/hx-location/

func (HXLocationResponse) String

func (hxl HXLocationResponse) String() string

String strings

type HXSwap

type HXSwap int64

HXSwap - https://htmx.org/attributes/hx-swap/

const (
	// SwapUnknown is the zero value for the enum
	SwapUnknown HXSwap = iota
	// SwapInnerHTML - The default, replace the inner html of the target element - probably no reason to ever set this explicitly
	SwapInnerHTML
	// SwapOuterHTML - Replace the entire target element with the response
	SwapOuterHTML
	// SwapBeforeBegin - Insert the response before the target element
	SwapBeforeBegin
	// SwapAfterBegin - Insert the response before the first child of the target element
	SwapAfterBegin
	// SwapBeforeEnd - Insert the response after the last child of the target element
	SwapBeforeEnd
	// SwapAfterEnd - Insert the response after the target element
	SwapAfterEnd
	// SwapDelete - Deletes the target element regardless of the response
	SwapDelete
	// SwapNone - Does not append content from response (out of band items will still be processed).
	SwapNone
)

func HXSwapFromString

func HXSwapFromString(s string) HXSwap

HXSwapFromString returns an [HXSWap] from its string representation

func (HXSwap) String

func (hxs HXSwap) String() string

String returns the string representation of a status code

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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