Let's Go Dynamic HTML templates › Template actions and functions
Previous · Contents · Next
Chapter 5.2.

Template actions and functions

In this section we’re going to look at the template actions and functions that Go provides.

We’ve already talked about some of the actions — {{define}}, {{template}} and {{block}} — but there are three more which you can use to control the display of dynamic data — {{if}}, {{with}} and {{range}}.

Action Description
{{if .Foo}} C1 {{else}} C2 {{end}} If .Foo is not empty then render the content C1, otherwise render the content C2.
{{with .Foo}} C1 {{else}} C2 {{end}} If .Foo is not empty, then set dot to the value of .Foo and render the content C1, otherwise render the content C2.
{{range .Foo}} C1 {{else}} C2 {{end}} If the length of .Foo is greater than zero then loop over each element, setting dot to the value of each element and rendering the content C1. If the length of .Foo is zero then render the content C2. The underlying type of .Foo must be an array, slice, map, or channel.

There are a few things about these actions to point out:

The html/template package also provides some template functions which you can use to add extra logic to your templates and control what is rendered at runtime. You can find a complete listing of functions here, but the most important ones are:

Function Description
{{eq .Foo .Bar}} Yields true if .Foo is equal to .Bar
{{ne .Foo .Bar}} Yields true if .Foo is not equal to .Bar
{{not .Foo}} Yields the boolean negation of .Foo
{{or .Foo .Bar}} Yields .Foo if .Foo is not empty; otherwise yields .Bar
{{index .Foo i}} Yields the value of .Foo at index i. The underlying type of .Foo must be a map, slice or array, and i must be an integer value.
{{printf "%s-%s" .Foo .Bar}} Yields a formatted string containing the .Foo and .Bar values. Works in the same way as fmt.Sprintf().
{{len .Foo}} Yields the length of .Foo as an integer.
{{$bar := len .Foo}} Assign the length of .Foo to the template variable $bar

The final row is an example of declaring a template variable. Template variables are particularly useful if you want to store the result from a function and use it in multiple places in your template. Variable names must be prefixed by a dollar sign and can contain alphanumeric characters only.

Using the with action

A good opportunity to use the {{with}} action is the view.tmpl file that we created in the previous chapter. Go ahead and update it like so:

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

{{define "main"}}
    {{with .Snippet}}
    <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}}
{{end}}

So now, between {{with .Snippet}} and the corresponding {{end}} tag, the value of dot is set to .Snippet. Dot essentially becomes the models.Snippet struct instead of the parent templateData struct.

Using the if and range actions

Let’s also use the {{if}} and {{range}} actions in a concrete example and update our homepage to display a table of the latest snippets, a bit like this:

05.02-01.png

First update the templateData struct so that it contains a Snippets field for holding a slice of snippets, like so:

File: cmd/web/templates.go
package main

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

// Include a Snippets field in the templateData struct.
type templateData struct {
    Snippet  models.Snippet
    Snippets []models.Snippet
}

Then update the home handler function so that it fetches the latest snippets from our database model and passes them to the home.tmpl template:

File: cmd/web/handlers.go
package main

...

func (app *application) home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")
    
    snippets, err := app.snippets.Latest()
    if err != nil {
        app.serverError(w, r, err)
        return
    }

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

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

    // Create an instance of a templateData struct holding the slice of
    // snippets.
    data := templateData{
        Snippets: snippets,
    }

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

...

Now let’s head over to the ui/html/pages/home.tmpl file and update it to display these snippets in a table using the {{if}} and {{range}} actions. Specifically:

Here’s the markup:

File: ui/html/pages/home.tmpl
{{define "title"}}Home{{end}}

{{define "main"}}
    <h2>Latest Snippets</h2>
    {{if .Snippets}}
     <table>
        <tr>
            <th>Title</th>
            <th>Created</th>
            <th>ID</th>
        </tr>
        {{range .Snippets}}
        <tr>
            <td><a href='/snippet/view/{{.ID}}'>{{.Title}}</a></td>
            <td>{{.Created}}</td>
            <td>#{{.ID}}</td>
        </tr>
        {{end}}
    </table>
    {{else}}
        <p>There's nothing to see here... yet!</p>
    {{end}}
{{end}}

Make sure all your files are saved, restart the application and visit http://localhost:4000 in a web browser. If everything has gone to plan, you should see a page which looks a bit like this:

05.02-02.png

Additional information

Combining functions

It’s possible to combine multiple functions in your template tags, using the parentheses () to surround the functions and their arguments as necessary.

For example, the following tag will render the content C1 if the length of Foo is greater than 99:

{{if (gt (len .Foo) 99)}} C1 {{end}}

Or as another example, the following tag will render the content C1 if .Foo equals 1 and .Bar is less than or equal to 20:

{{if (and (eq .Foo 1) (le .Bar 20))}} C1 {{end}}

Controlling loop behavior

Within a {{range}} action you can use the {{break}} command to end the loop early, and {{continue}} to immediately start the next loop iteration.

{{range .Foo}}
    // Skip this iteration if the .ID value equals 99.
    {{if eq .ID 99}}
        {{continue}}
    {{end}}
    // ...
{{end}}
{{range .Foo}}
    // End the loop if the .ID value equals 99.
    {{if eq .ID 99}}
        {{break}}
    {{end}}
    // ...
{{end}}