htmx for GO gin web development

Francis Stephan
5 min readMar 25, 2024

--

I wish to present my experience using htmxtechnology within the context of web development with golang and the gin web framework.

As per the htmx site, “htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext”. This article is not an htmxtutorial: the htmx docs are user friendly and quite extensive. It describes how I used htmxto develop my “chinese character dicitionary manager” (“gozdman”), showing some problems I had and how to solve them.

All source code is available on https://github.com/francisstephan/character_dictionary_manager

We shall have five sections:

  • using htmx to generate ajax requests,
  • two step calls,
  • adding a ‘Cancel’ button to the forms,
  • displaying server errors,
  • using htmx to manage keyboard shortcuts.

1. Using htmx to generate ajax requests

After /size GET request

All the buttons in the top line (Size, Zi => Pinyin, …) have htmx attributes and generate ajax requests based on these attributes.

For example, the Size button, when clicked, will start a GET AJAX request over the /size route, the result of which will overwrite the content (innerHTML) of the div element with CSS id "content" (further referred to as the #content div):

<button class = "menubouton" hx-get="/size" hx-target="#content" hx-swap="innerHTML" >Size</button>

The "#content" div source code within index.html :

<div id="content">{{ .content }}</div>

This div’s content may be overwritten in two ways:

  • with the GO templating system, through the {{.content}} tag
  • with the htmx ajax requests, such as here above, which will overwrite the text with the dictionary’s size.

The /size route is handled with the dicsize handler within zdman.go :

func dicsize(c *gin.Context) {
len, time := data.Dicsize()
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("The dictionary presently contains "+len+" entries ; last updated on "+time))
}

We do not need to bother where this text will be displayed: this was already specified in the hx-target = "#content" tag of the button element hereabove.

2. Two step requests:

Many interactions with the program will require two requests:

  • one to generate a form requiring user input (sending a GET request to generate this form)
  • one to process this form (sending a POST, PUT or DELETE request).

For instance, identifying a chinese character (Zi => Pinyin) is done in two steps (two AJAX requests):

  • one GET request:
<button class = "menubouton" hx-get="/getziform" hx-target="#content" hx-swap="innerHTML" >Zi => Pinyin</button>

The gin handler in `zdman.go` displays the form within the #content div:

func getziform(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(forms.Ziform()))
}

Since the program includes not less than 7 forms, we chose to have a forms golang module, containing all forms. The Ziform function contains the following form:

func Ziform() string {
return `
<form hx-post="/listzi" hx-target="#content" hx-swap="innerHTML" >
<label for="carac">Character:</label>
<input id="carac" name="carac" type="text" autofocus required minlength="1" maxlength="1">
<button class="menubouton" type="submit">Click to submit </button>
<button class="menubouton" hx-get="/remove">Cancel</button>
</form>
`
}

As a result, we get the following screen:

After first (GET) request
  • one POST request:

When the form here above is submitted, it will be processed as a POST request by the adequate handler (getziform within zdman.go), and the result will, once again, overwrite the #content div. If we enter the 陶 character, we get the following result:

After second (POST) request

3. Having a cancel button in our forms:

We tried the htmx-remove extension, but this did not satisfy our requirements.

We solved this by associating a specific GET request with these buttons, requesting for removal of the #content div's content:

<button class="menubouton" hx-get="/remove" hx-target="#content" hx-swap="innerHTML">Cancel</button>

and the corresponding handler in zdman.go:

func remove(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("Form canceled."))
}

4. Displaying server errors:

With HTMX (version 1.9.9) server errors are only shown in the console, which is not acceptable in production code. We found good information in https://xvello.net/blog/htmx-error-handling/ , and we used an EventListener in javascript, based on the htmx:afterRequest event:

window.onload = function() {
elem = document.body;
document.body.addEventListener('htmx:afterRequest', function (evt) {
contenu = document.getElementById("content");
if (evt.detail.failed && evt.detail.xhr) { // display error message within the document (and not only the console)
// Server error with response contents, equivalent to htmx:responseError
console.warn("Server error", evt.detail)
const xhr = evt.detail.xhr;
contenu.innerHTML = `Unexpected server error: ${xhr.status} - ${xhr.statusText}`;
}
});
};

5. Keyboard shortcuts:

We use 3 keyboard shortcuts:

  • z to load the Zi => Pinyin form,
  • p to load the Pinyin => Zi form, and
  • Esc to abort a form.

z and p eventlisteners should only be active when no form is displayed, while Esc should only be active in the opposite case, i.e. when a form is displayed. This is easily done using HTMX ajax functions. For instance, for the Esc key, the event listener (within footer.html) is :

function esckey(e) {
if(e.keyCode==27) htmx.ajax('GET', "/remove", {target: "#content", swap: "innerHTML"}); // Esc key : cancel form
}

The syntax of this function call is very similar to the htmx attributes of the Cancel button here above.

The function to add or remove key event listeners tests the presence of a <form > element in the #content div :

function ajustkey() {
contenu = document.getElementById("content");
if (contenu.innerHTML.startsWith("<form ")) { // if form, enable Esc and remove z and p listeners
document.body.removeEventListener("keydown", shortkey);
document.body.addEventListener("keydown", esckey);
}
else { // if not form, remove Esc (there is nothing to cancel) and enable z and p listeners
document.body.addEventListener("keydown", shortkey);
document.body.removeEventListener("keydown", esckey);
}
}

This function is triggered by the htmx:afterRequest event, through an event listener initially added to the page (remember, all htmx calls are ajax calls, meaning that the page is only loaded once):

window.onload = function() {
elem = document.body
elem.addEventListener("keydown", shortkey) // initially enable z and p shortcut keys
elem.addEventListener("htmx:afterRequest", ajustkey) // after ajax request performed by htmx, adjust keydown listeners
}

Conclusion:

As a conclusion we may say that:

  • HTMX makes for an elegant and simple program flow, using all HTTP verbs. Here we used 4 verbs: GET, POST, PUT and DELETE.
  • HTMX combines very well with go and the gin web framework (for which we propose a tutorial in https://github.com/francisstephan/gin-html-tutorial)

--

--