htmx for GO gin web development
I wish to present my experience using htmx
technology 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 htmx
tutorial: the htmx docs are user friendly and quite extensive. It describes how I used htmx
to 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
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:
- 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:
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)