htmx is a new javascript framework that tips the typical front-end development paradigm on its head. A typical webapp these days looks like:

While this has become the defacto model for front-end development in 2022, there are some problems with this approach:

Htmx recognises that:

Instead of returning JSON which is unmarshalled and processed again on the front-end, htmx lets you return already-rendered HTML, which is inserted/update directly on the page. Any element can make requests (GET/POST/PUT/PATCH etc) and there are various ways of triggering those requests (click, polling, typing, websockets).

At this point, if htmx is new to you, you might want to look at their introduction which is probably better at showcasing the benefits than I am.

As an aside, if this sounds “weird”, I don’t blame you. I’ve been in software development for almost 30 years, and this is one of the bigger paradigm shifts I’ve encountered. However, I think it is an approach that makes a lot of sense, and I’m having a lot of fun with it.

What we’d like to accomplish

For a work project, I needed:

(the last part is crucial to our effective use of htmx)

I found plenty of solutions for parts of these problems, but nothing complete. Additionally, I’m kind of a “from scratch” person, and I like to implement these sorts of things from the ground up - at least initially. It helps provide me with an understanding of the tools and technology which I don’t get if I just bundle someone’s middleware and call it done.

Set up the web server

I do make a concession here to frameworks and use gin gonic for our server. It makes the route handlers a little simpler. There is otherwise nothing specific to gin here, and you could adapt these techniques to any other platform.

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		c.String(200, "hello world")
	})

	r.Run()
}

If you run this, you should find that you can hit your web server on the default port (8080) and receive a response for requests to “/”. Nothing too earth-shattering yet.

Embed the templates

It’s time to start creating our templates and embedding them in our binary at build time. Create a sub-directory called templates and create a file index.html inside.

<!DOCTYPE html>
<html lang="en">
  <body>
  <h1>The number one is {{ 1 }}.</h1>
  </body>
</html>

You can see that we are including a template directive {{ 1 }} (that will return a literal ‘1’), just to prove we are processing this as a template and not a plain text file.

Now, modify your main.go to bring in the templates and to serve it up:

package main

import (
	"embed"
	"html/template"

	"github.com/gin-gonic/gin"
)

//go:embed templates
var templateFS embed.FS

func main() {
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		t := template.Must(template.ParseFS(templateFS, "templates/index.html"))
		t.Execute(c.Writer, nil)
	})
	
	r.Run()
}

We’ve made a fairly small number of changes, but introduced a few new things, let’s cover them one by one.

//go:embed templates
var templateFS embed.FS

This combination of comment and var declaration use the embed package to bring a virtual filesystem of files into our binary at build time.

The templates string is a path to a directory (from the point in the filesystem relative to the package we are building). The embed.FS implements fs.FS so that we can do things like Open("somefile") to access files, with our code pulling them directly from our binary.

t := template.Must(template.ParseFS(templateFS, "templates/index.html"))

The inner part (template.ParseFS) reads from our virtual filesystem (first argument) a particular file (templates/index.html) and parses and returns the parsed template.

The template.Must wrapper is a common go paradigm, it takes the two returns from ParseFS and simply panics if the error is not nil. It is equivalent to:

t, err := template.ParseFS(templateFS, "templates/index.html")
if err != nil {
	panic(err)
}

It recognises that errors in these sorts of cases are indicative of programmer error (you added a template with errors in it) and the best you can do is bail out immediately - there is no point in trying to handle this error.

t.Execute(c.Writer, nil)

Our template has been parsed, but not yet executed. The parsing and execution are separate steps, as you can re-use a template struct, passing in different data to render each time.

In this case we don’t have any data we need to send to our template to render it, so we pass a nil as the second argument.

The first argument is the io.Writer which is the actual response to our web client.

So - our template gets executed and the {{ 1 }} will get turned into a ‘1’. Compile and run your program, connect to your web server on http://localhost:8080 and you should see this:

Rendered template
Rendered template

Bringing in htmx

Let’s add some dynamic content, courtesy of htmx.

Add a new template inside the templates directory, called htmx_time.html:

<div hx-get="/htmx/time.html" hx-trigger="every 2s" hx-swap="outerHTML">
    {{ .ts }}
</div>

Now would be a good time to have a look at the excellent htmx documentation to see how the extra markup works. But it’s pretty straightforward to understand by reading it - every 2 seconds we will fetch /htmx/time.html.

The important part to note is that because of the hx-swap directive, the fetch replaces the entire <div> that it is in. The templated part ({{ .ts }}) will be our timestamp, each time we fetch this div we will be making a new server side request for this fragment which will replace the existing one, showing a new timestamp and setting us up for another refresh 2 seconds later.

The default for hx-swap is innerHTML, which replaces the elements contents, but not the element itself. You might ask - why not just replace the content, since the outer <div> that triggers the reloading will remain the same anyway.

There are certainly situations where you would want to do that. But in our case, there’s a couple of reasons why we prefer outerHTML:

  1. Firstly, we will leverage the template fragment consistency a little later on in our index page.
  2. Secondly, we might want to make a server side decision to stop the reloading every 2 seconds - if we make that decision we simply return a plain div with no hx- markup, and the reloading stops!

So, let’s wire up a route to serve up this template at the right place. Add this after the existing route for “/":

r.GET("/htmx/time.html", func(c *gin.Context) {
	t := template.Must(template.ParseFS(templateFS, "templates/htmx_time.html"))
	t.Execute(c.Writer, gin.H{"ts": time.Now().Format(time.Kitchen)})
})

You’ll also need to import the “time” package in your import statement at the top of main.go.

One new thing here - when we execute your new template, we are passing some data in. In this case it is a gin.H struct (which is just a map[string]interface, allowing us to send arbitrary data structure values, each with a string key). The key is “ts” (matching the .ts in our htmx_time.html template) and the value is a string with the current “kitchen time” (which is just hours and minutes plus an AM/PM indicator).

If you compile and run this, and fetch http://localhost:8080/htmx/time.html you should see the current time in your browser. It won’t do anything magical with respect to refreshing - this is just a HTML fragment at this point.

So let’s change index.html to include the htmx library, and to reference our new fragment:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/htmx.org@1.7.0"></script>
  </head>
  <body>
    <div hx-get="/htmx/time.html" hx-trigger="load" hx-swap="outerHTML">
    </div>
  </body>
</html>

The <div> here is slightly different to the one in htmx_time.html - instead of it reloading on a delay, it will load the new fragment from /htmx/time.html as soon as the page loads.

So - after / loads, htmx will see the hx-get and hx-trigger directives, which will load our fragment via our new route - replacing that div, and starting the 2 second refresh.

Run your program again and load up http://localhost:8080 - you should see a page which shows the time, updating every couple of seconds. You can verify this by just staring at it until the minute ticks over, or check the logs in your console:

[GIN] 2022/03/03 - 21:10:29 | 200 | 257.250µs | ::1 | GET "/"
[GIN] 2022/03/03 - 21:10:29 | 200 | 210.458µs | ::1 | GET "/htmx/time.html"
[GIN] 2022/03/03 - 21:10:31 | 200 | 196.791µs | ::1 | GET "/htmx/time.html"
[GIN] 2022/03/03 - 21:10:33 | 200 | 554.417µs | ::1 | GET "/htmx/time.html"
[GIN] 2022/03/03 - 21:10:35 | 200 | 498.083µs | ::1 | GET "/htmx/time.html"

You can see it loads the initial index page, then immediately the /htmx/time.html fragment. Every 2 seconds after that we refresh the /htmx/time.html fragment.

Remove the duplication

If you saw the almost identical <div> in the index as the one in htmx_time.html and thought it was sub-optimal - you were right. Let’s fix this right now.

Change index.html to look like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/htmx.org@1.7.0"></script>
  </head>
  <body>
    {{ template "htmx_time.html" .}}
  </body>
</html>

Instead of effectively writing our dynamic <div> twice, we are using the template directive to include it inline. Thus our page will have the current time shown on first load, as well as refreshing every 2 seconds.

However, there are hidden benefits to this technique. Because no javascript is involved on the initial page load, the page will show all of the content, both static and dynamic with the single HTTP request. It will be visible to the end user immediately (without them having to make another HTTP request to get the dynamic content) and it is visible to various “web crawler” robots as well.

This can be a huge win for both page response times and for search engine relevance. With typical SPA frontend/backend split setups, the page that a crawler sees is probably just an empty husk, with a bunch of empty <div>s and not much else. Additionally, end users no longer need to stare at a bunch of grey rectangles, waiting for the actual content to come in.

We aren’t quite done yet though. The {{ template }} directive isn’t “magic” - it doesn’t know anything about the filesystem, it won’t know how to dynamically “fetch” the second template when we load index.html. Let’s make the required changes to our routes.

Change the two routes to look like this:

r.GET("/", func(c *gin.Context) {
	t := template.Must(template.ParseFS(templateFS,
	                                    "templates/index.html",
										"templates/htmx_time.html"))
	t.Execute(c.Writer, gin.H{"ts": timeNow()})
})

r.GET("/htmx/time.html", func(c *gin.Context) {
	t := template.Must(template.ParseFS(templateFS,
	                                    "templates/htmx_time.html"))
	t.Execute(c.Writer, gin.H{"ts": timeNow()})
})

And add the new function we are using now to generate the time string:

func timeNow() string {
  return time.Now().Format(time.Kitchen)
}

As usual in Go, there’s little “magic”. If we want our output to contain the content from two templates, well, we have to specify both those templates in our ParseFS call.

When we specify {{ template "htmx_time.html" .}} in index.html it looks for a template within the parsed templates called htmx_time.html. That name is automatically derived from the filename when we called ParseFS here:

t := template.Must(template.ParseFS(templateFS,
                                    "templates/index.html",
                                    "templates/htmx_time.html"))

Our parsed template t now contains a named sub-template called htmx_time.html, which is why we can use it in the {{ template "htmx_time.html" }} directive. It’s important to note that the html/template directives know nothing about the filesystem - you have to prepare all you need before you Execute. I belabour this point a little because it tripped me up for a while, and because it’s potentially surprising behaviour if you are used to other languages and frameworks, which provide you with a more streamlined experience (dynamically loading templates from within other templates).

This “lack of magic” does mean you have to work harder, but it also (I believe) benefits your understanding, and allows you the ultimate flexibility to make it work in the way that fits your own application.

One other thing to note is the trailing ‘.’ in the directive {{ template "htmx_time.html" . }}. It’s important! Passing along the ‘.’ argument (known as the “pipeline” if you see the docs) means that the struct with the “ts” key containing our time value is passed along to the other template.

Try it out! Hit http://localhost:8080 in your web browser, and you should see the time, automatically refreshing. Verify that the inital page load contained the dynamic content as well by grabbing it with curl:

$ curl http://localhost:8080/
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/htmx.org@1.7.0"></script>
  </head>
  <body>
    <div hx-get="/htmx/time.html" hx-trigger="every 2s">
    10:00PM
	</div>
  </body>
</html>

Have fun with htmx and go templates!


Tags: golang  html/template  htmx  embedded