Let's Go Dynamic HTML templates › Displaying dynamic data
Previous · Contents · Next
Chapter 5.1.

Displaying dynamic data

Currently our snippetView handler function fetches a models.Snippet object from the database and then dumps the contents out in a plain-text HTTP response.

In this chapter we’ll improve this so that the data is displayed in a proper HTML webpage which looks a bit like this:

05.01-01.png

Let’s start in the snippetView handler and add some code to render a new view.tmpl template file (which we will create in a minute). Hopefully this should look familiar to you from earlier in the book.

File: cmd/web/handlers.go
package main

import (
    "errors"
    "fmt"
    "html/template" // Uncomment import
    "net/http"
    "strconv"

    "snippetbox.alexedwards.net/internal/models"
)

...

func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.NotFound(w, r)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    // Initialize a slice containing the paths to the view.tmpl file,
    // plus the base layout and navigation partial that we made earlier.
    files := []string{
        "./ui/html/base.tmpl",
        "./ui/html/partials/nav.tmpl",
        "./ui/html/pages/view.tmpl",
    }

    // Parse the template files...
    ts, err := template.ParseFiles(files...)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    // And then execute them. Notice how we are passing in the snippet
    // data (a models.Snippet struct) as the final parameter?
    err = ts.ExecuteTemplate(w, "base", snippet)
    if err != nil {
        app.serverError(w, r, err)
    }
}

...

Next up we need to create the view.tmpl file containing the HTML markup for the page. But before we do, there’s a little theory that I need to explain…

Any data that you pass as the final parameter to ts.ExecuteTemplate() is represented within your HTML templates by the . character (referred to as dot).

In this specific case, the underlying type of dot will be a models.Snippet struct. When the underlying type of dot is a struct, you can render (or yield) the value of any exported field in your templates by postfixing dot with the field name. So, because our models.Snippet struct has a Title field, we could yield the snippet title by writing {{.Title}} in our templates.

I’ll demonstrate. Create a new file at ui/html/pages/view.tmpl and add the following markup:

$ touch ui/html/pages/view.tmpl
File: ui/html/pages/view.tmpl
{{define "title"}}Snippet #{{.ID}}{{end}}

{{define "main"}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Title}}</strong>
            <span>#{{.ID}}</span>
        </div>
        <pre><code>{{.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Created}}</time>
            <time>Expires: {{.Expires}}</time>
        </div>
    </div>
{{end}}

If you restart the application and visit http://localhost:4000/snippet/view/1 in your browser, you should find that the relevant snippet is fetched from the database, passed to the template, and the content is rendered correctly.

05.01-02.png

Rendering multiple pieces of data

An important thing to explain is that Go’s html/template package allows you to pass in one — and only one — item of dynamic data when rendering a template. But in a real-world application there are often multiple pieces of dynamic data that you want to display in the same page.

A lightweight and type-safe way to achieve this is to wrap your dynamic data in a struct which acts like a single ‘holding structure’ for your data.

Let’s create a new cmd/web/templates.go file, containing a templateData struct to do exactly that.

$ touch cmd/web/templates.go
File: cmd/web/templates.go
package main

import "snippetbox.alexedwards.net/internal/models"

// Define a templateData type to act as the holding structure for
// any dynamic data that we want to pass to our HTML templates.
// At the moment it only contains one field, but we'll add more
// to it as the build progresses.
type templateData struct {
    Snippet models.Snippet
}

And then let’s update the snippetView handler to use this new struct when executing our templates:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.NotFound(w, r)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    files := []string{
        "./ui/html/base.tmpl",
        "./ui/html/partials/nav.tmpl",
        "./ui/html/pages/view.tmpl",
    }

    ts, err := template.ParseFiles(files...)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    // Create an instance of a templateData struct holding the snippet data.
    data := templateData{
        Snippet: snippet,
    }

    // Pass in the templateData struct when executing the template.
    err = ts.ExecuteTemplate(w, "base", data)
    if err != nil {
        app.serverError(w, r, err)
    }
}

...

So now, our snippet data is contained in a models.Snippet struct within a templateData struct. To yield the data, we need to chain the appropriate field names together like so:

File: ui/html/pages/view.tmpl
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}

{{define "main"}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Snippet.Title}}</strong>
            <span>#{{.Snippet.ID}}</span>
        </div>
        <pre><code>{{.Snippet.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Snippet.Created}}</time>
            <time>Expires: {{.Snippet.Expires}}</time>
        </div>
    </div>
{{end}}

Feel free to restart the application and visit http://localhost:4000/snippet/view/1 again. You should see the same page rendered in your browser as before.


Additional information

Dynamic content escaping

The html/template package automatically escapes any data that is yielded between {{ }} tags. This behavior is hugely helpful in avoiding cross-site scripting (XSS) attacks, and is the reason that you should use the html/template package instead of the more generic text/template package that Go also provides.

As an example of escaping, if the dynamic data you wanted to yield was:

<span>{{"<script>alert('xss attack')</script>"}}</span>

It would be rendered harmlessly as:

<span>&lt;script&gt;alert(&#39;xss attack&#39;)&lt;/script&gt;</span>

The html/template package is also smart enough to make escaping context-dependent. It will use the appropriate escape sequences depending on whether the data is rendered in a part of the page that contains HTML, CSS, Javascript or a URI.

Nested templates

It’s really important to note that when you’re invoking one template from another template, dot needs to be explicitly passed or pipelined to the template being invoked. You do this by including it at the end of each {{template}} or {{block}} action, like so:

{{template "main" .}}
{{block "sidebar" .}}{{end}}

As a general rule, my advice is to get into the habit of always pipelining dot whenever you invoke a template with the {{template}} or {{block}} actions, unless you have a good reason not to.

Calling methods

If the type that you’re yielding between {{ }} tags has methods defined against it, you can call these methods (so long as they are exported and they return only a single value — or a single value and an error).

For example, our .Snippet.Created struct field has the underlying type time.Time, meaning that you could render the name of the weekday by calling its Weekday() method like so:

<span>{{.Snippet.Created.Weekday}}</span>

You can also pass parameters to methods. For example, you could use the AddDate() method to add six months to a time like so:

<span>{{.Snippet.Created.AddDate 0 6 0}}</span>

Notice that this is different syntax to calling functions in Go — the parameters are not surrounded by parentheses and are separated by a single space character, not a comma.

HTML comments

Finally, the html/template package always strips out any HTML comments you include in your templates, including any conditional comments.

The reason for this is to help avoid XSS attacks when rendering dynamic content. Allowing conditional comments would mean that Go isn’t always able to anticipate how a browser will interpret the markup in a page, and therefore it wouldn’t necessarily be able to escape everything appropriately. To solve this, Go simply strips out all HTML comments.