An Exploratory Trip of templ, HTMX, and Go

Fri Oct 20 2023

Lukas Werner

So as of late I’ve been hearing more about these two technologies, templ and HTMX.

Like most Golang programmers I started writing my server side rendered pages in html/template. However, I would often get quite frustrated with the standard library’s template syntax. As someone who started web programming on Flask in Python I found the standard library’s templating just annoying for no reason. Jinja2 templates felt so expressive and easy to write and html/template was just cumbersome and didn’t feel like anything else familiar. It was so annoying that I even at times considered to getting Jinja2 working in Golang.

Templ - The Premier Golang Template Language

Enter a few weeks ago when I was listening to a Go Time episode about templ. And right away I was hooked on its expressiveness, like JSX, without any of the runtime overhead having everything done in JavaScript, instead all being done in a compile time adaptation of my document to string outputs.

Getting started with templ is relatively straight forward. Basically just install the templ transpiler and the extension(s) for your editor of choice. Then simply make a .templ file for your template, and now you can write these kinds of functions: (Also I apologize for the syntax highlighting in this article. Unfortunately my markdown renderer doesn’t support templ yet.)

package main

templ Hello(name string) {
    <h1>Hello {name}</h1>
}

But that is super simple. Where templ really shines is its ability to do loops and call regular Golang code directly in the template because it is just Golang that has bits of HTML strings inside of it.

package main

import "strings"

templ Friends(names []string) {
    <ul>
        for _, name := range names {
            <li>{strings.Split(name, " ")[0]}</li>
        }
    </ul>
}

This flexibility is just INCREDIBLE and unlike anything I’ve tried in Golang. (and yes I know that in rust you can just write HTML directly in it because of rusts fascinating macros system)

Unfortunately, writing just Golang code on the server side can be restrictive on client side interactivity because it is only capable of getting updates on full page refreshes and form submissions.

Enter HTMX

So like lots of people who spend way too much time on YouTube, I’ve been slowly following the Primeagen’s exploration and expressed hype for HTMX as a technology that allows for greater expression of reactivity for languages & technology stacks that historically have had issues with client side reactivity. Because of his hype I decided to take a stronger look at the technology and evaluate whether I should start incorporating it in my projects.

Little Side Tangent: I will mention that I’ve been working of my own sort of integrated solution of live reactivity in Golang with HTML. It is called lively.go and is basically just a websocket connection between the frontend and the backend kinda like Phoenix’s LiveView or what I understand Ruby’s Hotwire to be. It is still very early days for the way I have it built, and I may just end up loading it up with templ in order to get some better syntax ergonomics. However, the ability to send document updates based on a channel in Golang is just spectacular!

Using HTMX on the client side just feels magical. Just adding one script tag to the header and then having any interactivity be a hx- away is just wild! The use of shipping partial HTML documents is quite interesting as it allows for better component reuse and modularity when it comes to system design, just like react’s JSX.

But why?

For one because it reduces the shipped JavaScript which reduces the load times on non-ideal connections (e.g. cellular connections, low bandwidth, high latency etc.) Two, because outside HTMX and templ there are relatively few ways of doing client side reactivity from a server and require full page refreshes which is not a great user experience. If we’re validating a form and have to send it all the way to the server to make sure that it is correct and then send the whole document back. With HTMX we only need to send back one little bit that is relevant to the form.

This difference between ideologies could not be more apparent than the difference between Gmail and Hey.com. While Gmail is a client-side application that takes a few seconds to load while Hey.com loads almost instantly. But the benefits don’t stop there Hey.com continues to keep up with the speed local JavaScript once both apps are loaded up. An old school full refresh app has no chance of keeping up. Just look at the shift over the last 20 years from services like Yahoo Mail & Hotmail to services like Gmail.

So how does the stack handle with a ‘real’ app?

So in order to fully evaluate this potential stack setup I will be building a simple polling app. All the tech used will be Golang, templ, HTMX and sqlite (my favorite database).

I’m going to start leave out some of the boring “boilerplate” code and will only cover the unusual code.

Alright so let’s go through my routes. So in order to handle static files we use the standard library net/http’s FileServer.

//go:embed static
var staticFS embed.FS

s.router.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticFS)))

Alright so now we’re going to start using templ in our routes.

s.router.HandleFunc("/component/make-segments/{parts}", MakePollParts)

In this snippet we declare a component route with the name make-segments, and it takes a parts parameter. This allows us to use an arbitrary number of poll options by using a basic for loop in our template. Next up is the function definition where we make a regular net/http handler and grab out the parameter out and send it to the MakeSegments function. Which is then handled by the templ.Handler function which just returns a Handler.

func MakePollParts(w http.ResponseWriter, r *http.Request) {

	partsStr := mux.Vars(r)["parts"]
	parts, err := strconv.Atoi(partsStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	templ.Handler(MakeSegments(parts)).ServeHTTP(w, r)
}

And here is the templ definition of MakeSegments:

templ MakeSegments(c int) {
	for i := 0; i < c; i++ {
		<span>
			<label for={"part-"+strconv.Itoa(i)}>{"Poll Option #"+strconv.Itoa(i+1)+": "}</label>
			<input type="text" name={"part-"+strconv.Itoa(i)} placeholder={"Poll Option #"+strconv.Itoa(i+1)}/>
		</span>
	}
	<button hx-target="#make-segments" hx-post={"/component/make-segments/"+strconv.Itoa(c+1)}>Add Segment</button>
}

Alright so this is kinda dense, so lets break it down. So first off we loop over all the poll options and then make labels and IDs for every poll option. However, because templ is trying to protect us from injection we cannot just put a {var} inside an attribute instead we need to set the whole attribute value, which is why we string concatenate everything. (Theoretically this could be done with a fmt.Sprintf as well.) Then finally we create a button that references itself in the hx-post which will make a request to itself plus one in order to allow for incrementing the poll option size.

Side note: When working on this project I was kinda frustrated with having to constantly restart my go server and I finally took a more serious looked at Air (an application reloader for Golang). And OMG I can’t go back this is way too nice! I love the customizability with the .air.toml file which allows me to reload whenever my templ files change or literally anything else. I will note that adding Air directly causes issues when using templ generate because it will cause new go files to be made. That’s why I’d recommend adding _templ.go to the exclusion files list for exclude_regex.

Moving on. The landing page itself is fairly basic and is done using this templ definition.

templ HomePage() {
	<html>	
		<head>
			<title>Pollr</title>
			<link href="/static/styles.css" rel="stylesheet" />
			<script src="https://unpkg.com/[email protected]" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"></script>
		</head>
		<body>
			<header>
				<img src="/static/logo.png" id="logo"/>
			</header>
			<button hx-get="/make-poll" hx-swap="outerHTML">
				Create Poll
			</button>
		</body>
	</html>
}

Because this template definition doesn’t have any parameters we don’t need any additional logic for calling it. This is why we can have the following router definition.

s.router.Handle("/", templ.Handler(HomePage()))

But what makes the definition of the homepage interesting is the use of HTMX. Here, upon a button click, we load up a make-poll page which returns an HTML snippet and replaces the button while keeping the rest of the body intact.

Alright so next up we are working on our poll creation section. This is the first time we call another templ function definition inside a template. In this case we are calling our MakeSegments with a default of 2.

templ MakePoll() {
	<h1>Make Poll</h1>
	<form id="make-poll" action="/make-poll" method="POST">
		<span>
			<label for="name">Poll Name: </label>
			<input name="name" placeholder="Name" type="text"/>
		</span>
		<div id="make-segments">
		    @MakeSegments(2)
		</div>
		<button type="submit" hx-confirm="Make Poll?">Create</button>				
	</form>
}

Time to start saving our polls in our database. I will acknowledge that this probably isn’t the best schema for storing an arbitrary amount of items in a database column.

s.router.HandleFunc("/make-poll", func(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		templ.Handler(MakePoll()).ServeHTTP(w, r)
		return
	}
	if r.Method != "POST" {
        // ... Handle Errors
	}
	r.ParseForm()

	poll := Poll{
		URI:     randToken(),
		Name:    "",
		Options: []string{},
	}

	form := r.Form
    // ... Get name from form, save rest of form to options

	tx, err := s.db.Begin()
	if err != nil {
	    //... Handle Errors	
	}
	_, err = tx.Exec(`INSERT INTO polls values (?, ?, ?)`, poll.URI, poll.Name, strings.Join(poll.Options, "\t"))
	if err != nil {
	    // ... Handle Errors	

	}
	tx.Commit()

	http.Redirect(w, r, fmt.Sprintf("/vote/%s", poll.URI), http.StatusTemporaryRedirect)

})

Basically we just check if we get a get request then we return the template otherwise we get the form parts and save it into a struct, and then insert that into the database and then redirect the user to the voting page with their new poll link. The logic for that is elementary, so I’ll leave that as an exercise for the reader. But essentially it just grabs the poll from the db and extracts all the options and renders them in a templ page. And when the user makes their vote it then pushes their vote onto the votes db.

Displaying the finished votes is fairly simple, I just load up the (so far) very nice to use go-echarts and start loading in the data and render it to the DOM.

func (s *server) makeChart(id string) (string, error) {

	res := s.db.QueryRow(`select name, options from polls where id = ?`, id)
    // ... Handle Errors	
	var name, optionsStr string
	if err := res.Scan(&name, &optionsStr); err != nil {
		return "", err
	}

	options := strings.Split(optionsStr, "\t")

	rows, err := s.db.Query(`select idx from votes where id = ?`, id)
    // ... Handle Errors	
    // ... Make buckets of every vote and each section

	bar := charts.NewBar()
	bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
		Title: name,
	}))

    // ... convert the buckets to go-echarts the data format

    bar.SetXAxis(options).AddSeries("Votes", data)
	buf := new(bytes.Buffer)
	bar.Render(buf)

	return buf.String(), nil

}

And then the usage of that is the following:

chart, err := s.makeChart(pollID)
if err != nil {
    http.Error(w, "Not Found", http.StatusNotFound)
	log.Println("invalid opt name err: ", err)
	return
}
fmt.Fprint((*http.ResponseWriter), chart) // we just write it to the `ResponseWriter`

Overall the combination of HTMX and templ seems to fit really well together and is a tech stack that I find deserves to be used more often as an industry we try to ship less JavaScript. The first step on that journey is bringing more logic to the server. Which can be done by bringing the expressiveness that people in the JavaScript industry are used to, to the backend. I strongly believe that people who write JavaScript can adapt to Golang in under 2 weeks and be about 70-80% proficient. With technologies like templ we can help bring that expressiveness they need. And with HTMX we can go full circle bringing the interactivity that is expected of modern web apps.