Recently, I started working on sharepa again. While I still love the simplicity
and ease of use of htmx, the html/template
package of the Go standard library is
getting increasingly more frustrating as the templates grow more complex.
The issue I want to address in this blog post is passing down per-request
context information like information about the authenticated user to templates.
For simple templates, it is usally enough to pass this information to the
template alongside your data. But once you introduce nested templates or {{
block }}
directives, this quickly becomes a hard to maintain mess of inline
variables and scope-wrangling.
When faced with this issue again, I had enough and took a look at other Go
projects using plain old html/template
. Surely I can't be the only one annoyed
by this… right?
I found various solutions but the approach I liked the best is the one in use by forgejo. It works by overriding the functions available to the template just before rendering it.
Let's take a look at a sample application and how to use this pattern to clean up its template!
Imagine a small demo application that renders a list of projects and their associated tasks, as well as their current assignee. Innovative - right?
The following template renders the list:
{{ define "overview" }}
<html>
<body>
<h1>Overview</h1>
<p>Hello {{ .User }}!</p>
{{ $user := .User }}
{{ range .Projects }}
<h2>Project: {{ .Name }}</h2>
<ul>
{{ range .TODOs }}
<li>
{{ .Content }}
--
<span>
{{- if eq .Assignee $user -}}
You!
{{- else -}}
{{ .Assignee }}
{{ end }}
</span>
</li>
{{ end }}
</ul>
<hr/>
{{ end }}
</body>
</html>
{{ end }}
Saving the .User
data in a variable allows it to be used even after the root
scope changes within the range
expression.
This works as expected and shows a list of all projects, their TODOs and if the
current user is assigned, it substitues the value with You!
. But what if you
want to reuse this list in another template?
Let's try! First, wrap the list in a {{ block }}
which defines and immediately
executes a new template:
{{ define "overview" }}
<html>
<body>
<h1>Overview</h1>
<p>Hello {{ .User }}!</p>
{{ $user := .User }}
{{ range .Projects }}
<h2>Project: {{ .Name }}</h2>
{{ block "todo-list" .TODOs }}
<ul>
{{ range . }}
<li>
{{ .Content }}
--
<span>
{{- if eq .Assignee $user -}}
You!
{{- else -}}
{{ .Assignee }}
{{ end }}
</span>
</li>
{{ end }}
</ul>
{{ end }}
<hr />
{{ end }}
</body>
</html>
{{ end }}
panic: template: overview.html:16: undefined variable "$user"
And here's the issue! Variables don't get inherited by the newly defined template so the template refuses to compile. Ideally, I'd love to add the user as a parameter to the newly defined template but that's not possible using the default Go template engine.
The solution is to add a function retrieving the current user to the template
execution environment. The Go template package allows developers to define
custom template functions by providing a template.FuncMap
. At first, the
documentation scared me away by specifying that this must be called before the
template is parsed. But one interesting quirk of this function is that it is
legal to override elements!
To implement this, first specify a stub implementation when parsing the templates:
templates, err := template.New("").Funcs(map[string]any{
"currentUser": func() string { return "" },
}).ParseGlob("*.html")
This is required as otherwise the templates are not syntactically correct.
When rendering the template, this function can then be replaced by a more specific implementation:
templates.Lookup("overview").Funcs(map[string]any{
"currentUser": func() string { return req.URL.Query().Get("user") },
}).Execute(w,data)
Now you can use currentUser
instead of $user
in the template and it will be
replaced with the currently authenticated user.
With this in place, a new template can reuse the todo list like this:
{{ define "project" }}
<html>
<body>
<h1>Project: {{ .Name }}</h1>
{{ template "todo-list" .TODOs }}
<hr/>
</body>
</html>
{{ end }}
—
Using this pattern allowed me to greatly reduce the duplication in templates and
resulted in cleaner, more maintainable templates. This isn't only useful for
authenticaton information but applies to all information that needs to be
available everywhere. It is also not limited to returning strings. You can pass
entire structs in whatever shape you want to the templates. In my case, I use
this approach to define a ctx
function that allows me to access all relevant
information as a context.Context
wherever I need it. Other template functions
can also access these overridden runtime functions, although it's a bit more
tricky. I use this trick to define a {{ i18n }}
function that returns localized
strings:
func NewFuncMap(opts ...funcMapOption) template.FuncMap {
var out template.FuncMap
out = template.FuncMap{
"ctx": func() context.Context { return nil },
"i18n": func(key string, data ...any) template.HTML {
ctx := out["ctx"].(func() context.Context)()
s := translate(ctx, key, data...)
return template.HTML(s)
},
}
for _, o := range opts {
o.Apply(out)
}
return out
}
In the initial parsing step, I pass this function without any options. When rendering pages, I pass a customized ctx
function:
func (r *TemplateRenderer) renderWithContext(ctx context.Context, w http.ResponseWriter, name string, rd *RenderData) error {
tmpl := r.templates.Lookup(name)
tmpl = tmpl.Funcs(NewFuncMap(
WithFunc("ctx", func() context.Context {
return ctx
},
)))
return tmpl.Execute(w, rd)
}
I'm very happy to have found this pattern and can continue developing sharepa with as little external dependencies as possible!