Explaining Go + HTMX with examples

Raphaël Piccolin
6 min readDec 4, 2023

--

I’ll be showing some ways of using HTMX in a Go web server by going through examples from my project: https://github.com/raphael-p/beango-messenger. In these, I only use the Go standard library.

Although not featured here, templ is a great library for generating HTML with Go, I’d recommend checking it out.

Setting up your web server for HTMX

You will need:

  1. A Go web server (see my previous article).
  2. A directory (I called mine/resources) where you’ll put .js and .css files. The server must make it available to the client. To enable this, I added a resolver for GET requests to /resources :
import (
"net/http"
"path/filepath"
"runtime"
)

func relativeJoin(elem ...string) (string, bool) {
_, file, _, ok := runtime.Caller(1)
if !ok {
return "", false
}
path := []string{filepath.Dir(file)}
path = append(path, elem...)
return filepath.Join(path...), true
}

func(w *response.Writer, r *http.Request) {
path, ok := relativeJoin("../client/resources")
http.StripPrefix(
"/resources/",
http.FileServer(http.Dir(path)),
).ServeHTTP(w, r)
}

3. To add HTMX to your project by putting htmx.min.js in your /resources directory (https://htmx.org/docs/#download-a-copy).

4. To create script.js and style.css files in /resources.

Optional: I added json-enc.js to /resources (https://htmx.org/extensions/json-enc/). This makes the client encode/decode request bodies as application/json instead of application/x-www-form-urlencoded.
⚠️ I wouldn’t recommend doing this unless you have a specific reason to use JSON. URL-encoded data is easier to parse in Go and is the default format used by HTMX. I use this because I started building a JSON API before doing my front-end and don’t want to deal with two data formats in my server.

Go Templates

In the following examples I’ll be using Go templates to generate HTML. I won’t be explaining it much, so here’s a more in-depth explanation of how they work: https://gowebexamples.com/templates/.

TLDR: the html/template library is a tool to generate HTML from a template. For example, you could have this: var htmlSnippet string = `<div>{{ .SomeText }}</div>` and sub in a value for {{ .SomeText }}.

Example: login page

Here’s some code you can use to create a login page with HTMX:

import (
"html/template"
"net/http"
)

// The base template for all my pages
var Skeleton string = `<!DOCTYPE html>
<html>
<head>
<script src="/resources/htmx.min.js"></script>
<script src="/resources/json-enc.js"></script>
<script src="/resources/script.js"></script>
<link rel="stylesheet" type="text/css" href="/resources/style.css">
<title>Beango Messenger</title>
</head>
<body hx-on::before-request="clearErrorNodes();">
<div id="header">{{block "header" .}}{{end}}</div>
<div id="content">{{template "content" .}}</div>
<div id="footer">{{block "header" .}}{{end}}</div>
</body>
</html>`

// this will be inserted in <div id="content"> of Skeleton
var LoginPage string = `{{define "content"}}
<div id="login-form">
<form hx-ext="json-enc">
<div>
<label for="username">Username:</label>
<input type="text" name="username" maxlength="25">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" maxlength="25">
</div>
<div>
<button
hx-post="/login/login"
type="submit"
hx-swap="none"
>Log In</button>
<button
hx-post="/login/signup"
type="submit"
hx-swap="none"
>Sign Up</button>
</div>
<div id="errors"></div>
</form>
</div>
{{end}}`

// handler function for your endpoint
func Login(w *http.ResponseWriter, r *http.Request) {
serveTemplate(w, "loginPage", client.Skeleton+client.LoginPage, nil)
}

// creates a template and then passes it to the writer
func serveTemplate(w *http.ResponseWriter, name, value string, data map[string]any) {
newTemplate, err := template.New(name).Parse(value)

// handles template creation errors
if err != nil {
fmt.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}

// writes HTML to the http writer from the template and handles errors
if err := newTemplate.Execute(w, data); err != nil {
fmt.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
}

serveTemplate() will create a new template with every request. Since this would mean creating the same templates over and over again, I would highly recommend caching them by name in a map.

There’s a few interesting bits in here HTMX-wise:

hx-on

This attribute allows you to register a javascript callback against an event emitted on the node (or which bubbles to the node). I use it a lot because it keeps logic in the HTML, which is a major reason to use HTMX in the first place. Notice in the example above:

<body hx-on::before-request="clearErrorNodes();">

where clearErrorNodes() is a function defined in my resources/script.js:

// erases all elements with the id "errors"
const clearErrorNodes = () => {
const errorNodes = document.querySelectorAll('#errors');
for (const errorNode of errorNodes) errorNode.innerHTML = "";
};

This line will run the function whenever the htmx:before-request event is emitted in the document body (see list of HTMX events).

hx-on::<event> is shorthand for hx-on:htmx:<event>. If you want to set a handler for a custom event on a node, the syntax would be hx-on:my-custom-event="<javascript code>" (: vs ::).

Submitting a form

<form hx-ext="json-enc">
<div>
<label for="username">Username:</label>
<input type="text" name="username" maxlength="25">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" maxlength="25">
</div>
<div>
<button hx-post="/login/login" type="submit" hx-swap="none">Log In</button>
<button hx-post="/login/signup" type="submit" hx-swap="none">Sign Up</button>
</div>
</form>

The form node will submit a POST request to either login/login or login/signup (depending on which button is clicked) with a JSON body that has the username and password keys.

Using json-enc

<form hx-ext="json-enc">

Invoking the json-enc extension will convert the form’s body from URL-encoded to JSON it is submitted. Having json-enc.js in the document won’t automatically do this for all POST requests, it needs to be specified each time.

As stated before, the json-enc extension is completely optional, and you should only use it if you have a good reason to use JSON instead of URL-encoded data in your server.

Example: an event-based flow

HTMX enables you to define a logical sequence of actions within a node, like this:

<textarea
class="input-value message-input"
placeholder="Type your message"
name="content"
maxlength="5000"
hx-ext="json-enc"
hx-post="/home/chat/{{ .ID }}/sendMessage"
hx-trigger="send-message consume"
hx-swap="none"
hx-on:keypress="sendMessageOnEnter(event)"
hx-on::after-request="if(event.detail.successful) this.value = '';"
></textarea>

The flow is defined by these three attributes:

  1. hx-on:keypress="sendMessageOnEnter(event)"
2. hx-trigger="send-message consume"
3. hx-on::after-request="if(event.detail.successful) this.value = '';"

1. sendMessageOnEnter() will be called every time a key is pressed in textarea. It’s defined in my resources/script.js:

// Event handler, emits "send-message" if enter is pressed with no modifier key
const sendMessageOnEnter = (event) => {
if (event.key === "Enter" && !(event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)) {
event.preventDefault();
htmx.trigger(event.target, "send-message");
}
};

When ENTER is pressed, it will emit “send-message” on the textarea .

2. hx-trigger will fire off a POST request upon receiving “send-message”. The “consume” modifier stops event propagation.

3. hx-on::after-request defines a callback for when HTMX is done with the request. In this case, it erases textarea's content after a message is sent.

Fun fact: there used to be a bug in the HTMX project around having multiple hx-on attributes in a single HTML node. I made a fix for it.

Example: Infinite scroll

var messagePane string = `<table id="message-table">` + 
messageRows + `</table>`

// Displays messages as rows, but the first row contains some
// HTMX logic for fetching previous messages.
var messageRows string = `
{{ range $i, $m := .Messages }}
{{ if (eq $i 0) }}
<tr
hx-get="/home/chat/{{ $.ID }}/scrollUp?to={{ $.ToMessageID }}"
hx-swap="none"
hx-trigger="intersect once"
class="list-item"
>
<td class="cue">{{ $m.UserDisplayName }}</td>
<td class="message">{{ $m.Content }}</td>
</tr>
{{ else }}
<tr class="list-item">
<td class="cue">{{ $m.UserDisplayName }}</td>
<td class="message">{{ $m.Content }}</td>
</tr>
{{ end }}
{{ end }}`

messages := []struct{
Content string
UserDisplayName string
}{
{"Hi, how are you?", "Susan"},
{"I'm well", "Nigel"},
{"How are the grandkids?", "Nigel"},
}

chatData := map[string]any{
"Name": "the name of the chat",
"Messages": messages,
"ID": 6,
"ToMessageID": 46
}

client.ServeTemplate(w, "messagePane", client.MessagePane, chatData)

This codes makes a batch of messages visible to the user. In my CSS, I use flex-direction: column-reverse; on the table, so the newer messages are displayed at the bottom .When the user scrolls up to the first message of the batch, then the next batch of (older) messages are fetched.

The template syntax is a bit more complicated here: essentially it loops over messages, but defines the first message differently: it contains instructions to fetch the next batch.

On that first message, hx-trigger="intersect once" will trigger a request as soon as the tr on which it is defined becomes visible to the user. The “once” modifier means it will only trigger once in the node’s lifetime.

You might notice I am using hx-swap="none" a lot in these examples. This instructs the client to NOT swap out the node making a request with its response. Instead, I use hx-swap-oob in the response HTML to tell the client where to swap it in:

var messagePaneScroll string = `
<table
id="message-table"
hx-swap-oob="beforeend">`
+ messageRows
+ `</table>`

hx-swap-oob="beforeend" means that the rows should be appended to the end of the table. This way we can append some rows to the table without having to replace it.hx-swap-oob is especially useful if there are several nodes we wish to swap from a response: each node can have it’s own hx-swap-oob.

That’s all, thanks ✨

--

--

Raphaël Piccolin
Raphaël Piccolin

Written by Raphaël Piccolin

I'm a backend software engineer based in London. bio: https://raphael-p.com

Responses (1)