Frontend Development with Go Templates and HTMX

Wondering how to build a web frontend in Go? Then this article might be for you. It will explain how to use the text/template package of the Go standard library and htmx to build an interactive web UI.

Getting started by building the web server

Let´s start by building a simple web server with Go´s net/http package to serve our frontend. If you are new to building web servers with Go, you can find a very nice introduction on Eli Bendersky´s blog here.

import (	
    "html/template"
    "net/http"
)

type StarterServer struct {
	Mux       *http.ServeMux
	Templates map[string]*template.Template
}

func NewStarterServer() *StarterServer {
	server := &StarterServer{
		Mux:       http.NewServeMux(),
		Templates: templates.ParseTemplates(),
	}
	
	server.registerRoutes()

	return server
}

func (s *StarterServer) registerRoutes() {
    // Page Handlers
    s.Mux.HandleFunc("GET /", s.homeView)
    s.Mux.HandleFunc("GET /about", s.aboutView)
    
    // Fragment Handlers
    s.Mux.HandleFunc("GET /check-number", s.checkNumber)
    
    // Static
    s.Mux.Handle("GET /static/*", http.FileServer(http.FS(static)))
}

The server struct holds all dependencies that the server will use. In our case it will need access to the templates which shall be rendered. Moreover, because I like to have all routes in a single place, we register these in an extra method. All the handlers will be explained later in this post. At this point we need to dive into templates.ParseTemplates(), which is the function to initially prepare all our templates.

Template parsing

First, have a look at the official documentation of the text/template package here. Before a template can be executed and rendered, it must be parsed. This can be done once in the beginning if the parsed templates are stored somehow. A practical solution is to use a map, where you can have an entry for each parsed template.

const (
	IndexTemplateName = "index.gohtml"
	HomeTemplate      = "home"
	AboutTemplate     = "about"
)

func ParseTemplates() map[string]*template.Template {
	parsedTemplates := make(map[string]*template.Template)

	parsedTemplates[HomeTemplate] = template.Must(parseLayout().ParseFiles("templates/home.gohtml"))
	parsedTemplates[AboutTemplate] = template.Must(parseLayout().ParseFiles("templates/about.gohtml"))

	return parsedTemplates
}

func parseLayout() *template.Template {
	return template.Must(template.ParseFiles("templates/header.gohtml", "templates/index.gohtml"))
}

The template.ParseFiles() function is used to parse multiple templates into one executable template. It is important that the layout is parsed first and only then the specific page like home.gohtml. Also, constants are used because the names will also be utilized inside the handlers.

Now, the last thing missing is the actual templating. So let´s find out how to do this.

Go templates and composition

Let´s assume we are building a standard web UI with a fixed header and a dynamic content part. This is a useful scenario where we can also utilize template composition to simplify our templates. First, we need to specify the base layout of our UI. It contains some obligatory HTML tags like html, head and body.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
        <meta name="description" content="Web UI Starter"/>
        
        <title>{{ template "title" }}</title>
    </head>
    <body>
        {{ template "header" }}
        <main class="container">
            {{ template "content" . }}
        </main>
    </body>
</html>

You may have noticed the {{ template "some name" }} expressions. These define other templates, which will be rendered in that place. For a better understanding of this concept, have a look at the header:

{{ define "header"}}
<header>
    <nav>
        <div>This is the header, rendered on all pages</div>
        <a href="/">Home</a>
        <a href="/about">About</a>
        <hr>
    </nav>
</header>
{{ end }}

Here you define what will be rendered in place of the {{ template "header" }} section inside the main layout. Because the header will be the same on every page, we put it inside the parseLayout() function you have seen above. However, the most interesting part is the <main> section of the layout. This is the part of the UI which changes on every site. A simple example for that is the "About" page. It defines all the other templates besides the "header" template with static data.

{{ define "title" }}
    About Me
{{ end }}
{{ define "content" }}
    This is an example page to demonstrate template composition with Go.
{{ end }}
{{ define "footer" }}
    This is the footer of the about page.
{{ end }}

This page can then be rendered via the following handler:

func (s *StarterServer) aboutView(w http.ResponseWriter, req *http.Request) {
	err := s.Templates[templates.AboutTemplate].ExecuteTemplate(w, templates.IndexTemplateName, nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

The handler accesses the already parsed templates by using the map inside the server struct. The template is rendered by calling ExecuteTemplate() on the response writer w. Because the "About" page does not contain any dynamic data, we just pass nil as the data to the template.

Passing data into templates

At this point we covered most of the basics. However, one important piece is still missing. How do we render dynamic data inside our templates? The key for that is the "dot", which comes after the template name.

{{ template "content" . }}

The dot stands for the data structure passed into the template by the handler. Assume the following data structure:

type Person struct {
	Name string
	Age int
}

You can then pass some persons into the "Home" template by adding it to the ExecutTemplate() function instead of nil.

func (s *StarterServer) homeView(w http.ResponseWriter, req *http.Request) {
    persons := []Person{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 40},
    }
	
	err := s.Templates[templates.HomeTemplate].ExecuteTemplate(w, templates.IndexTemplateName, persons)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

The home template now has access to this slice via the "dot". Inside the curly braces you can have expressions like range and if to navigate the data. A little example could look like this:

{{ define "title" }}
    Home
{{ end }}
{{ define "content" }}
    {{ range . }}
        <div class="form-control">
            <p>Name: {{ .Name }}</p>
            <p>Name: {{ .Age }}</p>
        </div>
    {{ end }}
{{ end }}

It is worth mentioning that the data fields must be exported (start with a capital letter). Otherwise, the template cannot access the fields.

And that´s it for the basic building blocks of Go templates. You can read more about all available expression in the mentioned documentation of the text/template package

Using htmx to render templates only partially

Finally, I want to show you a really neat technique to add some interactivity to the templates without using JavaScript. For that, we will use htmx, which gives access to AJAX functionality directly inside HTML. I won´t explain more of htmx here, so be sure to at least read about the basics in its documentation. Also, we will combine the interactive element with rendering only parts of a template (also called template fragments).

To use htmx, we need to add htmx to our frontend by putting it inside our head tag:

<script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>

Now we are ready to go! As an example, we will implement a classic guessing game. Thus, the user must guess a secret number. If the guessed number is incorrect, the user gets a hint to guess either higher or lower. Htmx is used to send requests with the guessed number to our handler, which will then render only template fragment where the hint should be shown. To do this we make use of the block and with keywords. Have a look:

{{ define "content" }}
    <form hx-get="/api/check-number"
          hx-swap="innerHTML"
          hx-target="#response-container"
          hx-indicator="#indicator">
        <label>Number Guess:
            <input type="number" id="numberGuess" name="numberGuess"/>
        </label>

        <div id="response-container">
            {{ with . }}
                {{ block "guess-response" . }}
                    {{ if .IsCorrect }}
                        Congrats. The Guess {{ .Guess }} was right!
                        <br>
                    {{ else }}
                        {{ .Hint }}!
                        <br>
                    {{ end }}
                {{ end }}
            {{ end }}
        </div>

        <button type="submit">Send</button>
        <span id="indicator" class="htmx-indicator">Loading...</span>
    </form>
{{ end }}

We define a form where the user can enter his guess. When hitting the submit button a request will be sent to /api/check-number. The response will be rendered inside the div with the id "response-container" (hx-target="#response-container"). By using {{ with . }}, we make sure that the inside is only rendered if data is present (there is a response). Check out the logic inside the handler:

const secretNumber = 42

type guessResponse struct {
	IsCorrect bool
	Hint      string
	Guess     int
}

func (s *StarterServer) checkNumber(w http.ResponseWriter, req *http.Request) {
	var response guessResponse
	guessedNumber := req.FormValue("numberGuess")
	guess, _ := strconv.Atoi(guessedNumber)

	if guess > secretNumber {
		response = guessResponse{
			IsCorrect: false,
			Hint:      "Go lower",
			Guess:     guess,
		}
	} else if guess < secretNumber {
		response = guessResponse{
			IsCorrect: false,
			Hint:      "Go higher",
			Guess:     guess,
		}
	} else {
		response = guessResponse{
			IsCorrect: true,
			Hint:      "",
			Guess:     guess,
		}
	}

	err := s.Templates["home"].ExecuteTemplate(w, "guess-response", response)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

It reads the guessed number from the form parameter "numberGuess" (defined inside the input tag) and checks against the secret number. The response struct is created accordingly and put into the ExecuteTemplate() function. In this case the executed template is not one of the parsed ones but the template fragment defined by the block keyword. So only this fragment will be rendered again.

And there you have it, a simple example for using Go template "blocks" with htmx. I hope you find this useful. The complete source code is available on GitHub here and can be used as a starting point for frontend projects.

Bye for now :)

Niklas


Published on: 22. August 2024

Back