Rendering fields with HTML templates in Go
Go’s standard library provides HTML templating. Together with the net/http package, you can very easily build a service that renders a webpages. Asserting fine-grained control over templates and page rendering becomes a bit of a challenge in a complex use case where context drives how a page should be rendered. A typical use case is showing parts of structured content - a product, a news article, an event,… - depending on a logged in users role, or the type of content which is rendered.
Go’s HTML templating provides define
, template
, yield
and block
functions. These promote template reusability, maintainability and flexible implementations. There’s a risk that you may end up incorporating too much application logic in your templates, depending on your use case’s requirements.
A common pattern used in programming is the Model-View-Presenter pattern. It’s a derivative of the ubiquitous Model-View-Controller pattern. It differs in that the application logic is pushed towards a Presenter which acts as a middle man. The MVP pattern seems like a good candidate to mitigate the risk of generating complex templates.
I’ve build a small prototype. I’ll let the code speak for itself:
package main
import (
"bytes"
"html/template"
"net/http"
"strings"
)
func main() {
http.HandleFunc("/article", articleController)
http.ListenAndServe(":8080", nil)
}
func articleController(w http.ResponseWriter, r *http.Request) {
// This is our Model
var article = &Article{
PublicationType: "article",
Title: "A journal article title",
AlternativeTitle: "An alternative journal article title",
Body: "Here's the body text of the article",
Author: "Jon Doe",
Type: "Newsarticle",
}
// Compose a view with the presenter and the article
var presenter = &ArticlePresenter{article}
var view = &ArticleView{presenter}
// Let the view render and pass the output to an io.Writer
w.Header().Set("Content-Type", "text/html")
view.Render(w)
}
type Article struct {
PublicationType string
Title string
AlternativeTitle string
Body string
Author string
Type string
}
// The view is only responsible for rendering an input to HTML.
type View interface {
Render()
}
type ArticleView struct {
presenter Presenter
}
func (av *ArticleView) Render(w http.ResponseWriter) {
// Let the presenter process the article and return a renderable object e.g. a map.
tree := av.presenter.Process()
// Walk through the tree recursively and call all rendering functions
content := template.HTML(doRender(tree))
// Wrap the HTML output in an article template
article := `
<div class="article">
<h1>\{\{.Title\}\}</h1>
\{\{ .Content \}\}
</div>
`
t, _ := template.New("article").Parse(article)
t.ExecuteTemplate(w, "article", struct {
Title string
Content template.HTML
}{"A news article", content})
}
// A tree walker function which triggers rendering
func doRender(element interface{}) string {
var output []string
tree, ok := element.(map[string]*FieldSet)
if ok {
for _, fieldset := range tree {
output = append(output, fieldset.Render())
}
return strings.Join(output, "")
}
return strings.Join(output, "")
}
// The Presenter creates a renderable object
type Presenter interface {
Process() map[string]*FieldSet
}
type ArticlePresenter struct {
Article *Article
}
// Processes the Article object according to business rules / requirements.
func (ap *ArticlePresenter) Process() map[string]*FieldSet {
tree := make(map[string]*FieldSet)
items := []*TextField{}
items = append(items, &TextField{
Label: "Title",
Value: ap.Article.Title,
})
items = append(items, &TextField{
Label: "Alternative Title",
Value: ap.Article.AlternativeTitle,
})
items = append(items, &TextField{
Label: "Copy",
Value: ap.Article.Body,
})
tree["content"] = &FieldSet{
Label: "Content",
Items: items,
}
if ap.Article.PublicationType == "article" {
items := []*TextField{}
items = append(items, &TextField{
Label: "Author",
Value: ap.Article.Body,
})
items = append(items, &TextField{
Label: "Type",
Value: ap.Article.Body,
})
tree["metadata"] = &FieldSet{
Label: "Metadata",
Items: items,
}
}
return tree
}
// Define field types
type Field interface {
Render() string
}
type FieldSet struct {
Label string
Items []*TextField
}
func (fs *FieldSet) Render() string {
tpl := `
<div class="fieldset">
<h2>\{\{ .Label \}\}</h2>
<ul class="items">
\{\{ range .Items \}\}
<li>\{\{ . \}\}</li>
\{\{ end \}\}
</div>
</div>
`
// Render children
var items []template.HTML
for _, item := range fs.Items {
items = append(items, item.Render())
}
// Render the fieldset as a whole
t := template.Must(template.New("fieldset").Parse(tpl))
buf := &bytes.Buffer{}
err := t.Execute(buf, struct {
Label string
Items []template.HTML
}{Label: fs.Label, Items: items})
if err != nil {
panic(err)
}
return buf.String()
}
type TextField struct {
Label string
Value string
}
func (tf *TextField) Render() template.HTML {
tpl := `
<p><strong>\{\{ .Label \}\}</strong>: \{\{ .Value \}\}</p>
`
t := template.Must(template.New("field").Parse(tpl))
buf := &bytes.Buffer{}
err := t.Execute(buf, struct {
Label string
Value string
}{Label: tf.Label, Value: tf.Value})
if err != nil {
panic(err)
}
return template.HTML(buf.String())
}
Of course, there’s a lot that could be optimized here. There’s the repetition of templating calls across the Render()
methods. There’s the doRender()
function which I could replace entirely with a dedicated ArticleTree
struct. There’s the fact that a map
structure doesn’t guarantee that the order of elements returned will be the order in which they were inserted. I’m not going to go into those.
I can spot a few take aways here.
A lot of moving parts are isolated into interchangeable, reusable composable structs, and the ArticleController
becomes really lean. This allows you to organize your code architecture in a very clean and maintainable way without losing readability.
The context specific application logic is entirely tied up into the Process()
method of a Presenter
. A change in business constraints doesn’t mean you have to do a major rewrite of existing code everywhere. You can even create a new concrete Presenter
if too much complexity gets tied up in a single Presenter
.
Template reusability is still promoted. You can create new field types to organize and re-organize the constituent parts of the web page. Generation of concrete HTML output is delegated to the various implementations of field types.
This is a pattern which tends to be used in content management systems build in interpreted languages like Drupal. A lot of the heavy lifting, rendering discrete pieces of HTML and composing them, happens at run time. From a performance view, this would be a drawback. Established content management systems come with extensive caching affordances to mitigate performance issues.
This pattern would work well in concrete, delineated use cases with a limited scope. It’s a pattern that fits well in code that adhere to hexagonal or clean architecture principles.