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 usingtempl 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 forexclude_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.