34. HTML templates — Homework solutions

The .go solution files are in exercises/34/homework/solutions/. The challenge is a module, so go into its folder to run it:

cd exercises/34/homework/solutions/04-players
go run .

Problem 1 — A profile page

Problem. Show three fields of a struct on a page.

How to think about it. The fields need capital letters, because the template package has to see them. The struct can be made inside the handler. One {{.Field}} for each value, and the title can use one as well. A struct with an int in it needs no conversion: the template prints it.

Worked solution.

package main

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

type profile struct {
    Name  string
    City  string
    Level int
}

const profileText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Profile of {{.Name}}</title>
</head>
<body>
<h1>{{.Name}}</h1>
<p>City: {{.City}}</p>
<p>Level: {{.Level}}</p>
</body>
</html>
`

var profileTemplate = template.Must(template.New("profile").Parse(profileText))

func handleHome(w http.ResponseWriter, r *http.Request) {
    data := profile{Name: "Keiko", City: "Bandung", Level: 7}

    var page bytes.Buffer
    err := profileTemplate.Execute(&page, data)
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err := http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

The lines of the body that hold the data:

<title>Profile of Keiko</title>
<h1>Keiko</h1>
<p>City: Bandung</p>
<p>Level: 7</p>

Common mistakes.

  • Lower case fields. The compiler accepts them, and the page fails when it runs.
  • Writing {{Name}} with no dot. Go stops at the start with function "Name" not defined, because a word with no dot is the name of a function.
  • Importing text/template. Everything looks right until a visitor's text reaches it.

Problem 2 — A task list

Problem. A list with a mark on finished tasks, and a message for an empty list.

How to think about it. Execute gets the slice itself, so the dot is the slice, and {{range .}} goes over it. Inside the range, the dot is one task. {{if .Done}} adds the extra text, and needs its own {{end}}. {{else}} belongs to the range, and it comes before the range's {{end}}.

Worked solution.

package main

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

type task struct {
    Name string
    Done bool
}

var tasks = []task{
    {Name: "Install Go", Done: true},
    {Name: "Write a server", Done: true},
    {Name: "Finish this homework", Done: false},
}

const tasksText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Tasks</title>
</head>
<body>
<h1>Tasks</h1>
<ul>
{{range .}}
<li>{{.Name}}{{if .Done}} (done){{end}}</li>
{{else}}
<li>Nothing to do.</li>
{{end}}
</ul>
</body>
</html>
`

var tasksTemplate = template.Must(template.New("tasks").Parse(tasksText))

func handleHome(w http.ResponseWriter, r *http.Request) {
    var page bytes.Buffer
    err := tasksTemplate.Execute(&page, tasks)
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err := http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

The list in the body, with blank lines left out:

<li>Install Go (done)</li>
<li>Write a server (done)</li>
<li>Finish this homework</li>

The handlers only read tasks, and nothing ever changes it, so they can share it safely. Chapter 35 explains what changes when a handler changes shared data.

Common mistakes.

  • Writing {{.Tasks}}. The dot is already the slice here, and a slice has no fields.
  • Forgetting an {{end}}. Go stops at the start with unexpected EOF.

Problem 3 — A safe greeting

Problem. Show a name from the visitor on an HTML page, safely.

How to think about it. This is the handler from Chapter 33's homework with a template in place of fmt.Fprintf. The check stays the same. Nothing needs to be escaped by hand, because the package does that. The reply is now HTML, so the Content-Type changes.

Worked solution.

package main

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

type greetPage struct {
    Name string
}

const greetText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello</title>
</head>
<body>
<p>Hello, {{.Name}}!</p>
</body>
</html>
`

var greetTemplate = template.Must(template.New("greet").Parse(greetText))

func handleGreet(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        http.Error(w, "Please give a name.", http.StatusBadRequest)
        return
    }

    var page bytes.Buffer
    err := greetTemplate.Execute(&page, greetPage{Name: name})
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

func main() {
    http.HandleFunc("GET /greet", handleGreet)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err := http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

The line that holds the name, for two requests:

curl.exe "http://localhost:8080/greet?name=Keiko"
<p>Hello, Keiko!</p>

curl.exe -G --data-urlencode "name=<b>Keiko</b>" http://localhost:8080/greet
<p>Hello, &lt;b&gt;Keiko&lt;/b&gt;!</p>

Common mistakes.

  • Using template.HTML(name) to stop the escaping because a name looks wrong. That type tells the package to trust the value, which turns the protection off.
  • Escaping the name yourself as well. The package would escape your escaping, and the visitor would see &lt;.

Challenge — Players from a template file

Problem. Filter a read-only list by a value from the query string, with the template in a file.

How to think about it. No min at all is allowed, so only a value that is present gets converted. A new slice holds the players to show, which leaves allPlayers as it was. That matters: handlers share allPlayers, so they must not change it. The empty shown makes the template's {{else}} run when nobody qualifies.

The form keeps the visitor's min as a string and shows it again. It has been checked by then, and the template would escape it anyway.

Worked solution. templates/players.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Players</title>
</head>
<body>
<h1>Players</h1>
<form action="/" method="get">
<p><label>Lowest score: <input type="text" name="min" value="{{.Min}}"></label></p>
<p><button type="submit">Filter</button></p>
</form>
<ul>
{{range .Players}}
<li>{{.Name}}: {{.Score}}</li>
{{else}}
<li>No player has that many points.</li>
{{end}}
</ul>
</body>
</html>

main.go:

package main

import (
    "bytes"
    "fmt"
    "html/template"
    "net/http"
    "strconv"
)

type player struct {
    Name  string
    Score int
}

type playersPage struct {
    Min     string
    Players []player
}

// Nothing changes this slice, so the handlers can share it.
var allPlayers = []player{
    {Name: "Keiko", Score: 70},
    {Name: "Arif", Score: 45},
    {Name: "Ada", Score: 90},
}

var playersTemplate *template.Template

func handleHome(w http.ResponseWriter, r *http.Request) {
    minText := r.URL.Query().Get("min")

    lowest := 0
    if minText != "" {
        number, err := strconv.Atoi(minText)
        if err != nil {
            http.Error(w, "min must be a whole number.", http.StatusBadRequest)
            return
        }
        lowest = number
    }

    shown := []player{}
    for _, p := range allPlayers {
        if p.Score >= lowest {
            shown = append(shown, p)
        }
    }

    data := playersPage{Min: minText, Players: shown}

    var page bytes.Buffer
    err := playersTemplate.Execute(&page, data)
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

func main() {
    parsed, err := template.ParseFiles("templates/players.html")
    if err != nil {
        fmt.Println("Could not read the template:", err)
        fmt.Println("Run this program from its own folder, and check the template.")
        return
    }
    playersTemplate = parsed

    http.HandleFunc("GET /{$}", handleHome)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err = http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

The results, with only the list lines of the body:

curl.exe http://localhost:8080/
<li>Keiko: 70</li>
<li>Arif: 45</li>
<li>Ada: 90</li>

curl.exe "http://localhost:8080/?min=50"
<li>Keiko: 70</li>
<li>Ada: 90</li>

curl.exe "http://localhost:8080/?min=1000"
<li>No player has that many points.</li>

curl.exe -i "http://localhost:8080/?min=abc"
HTTP/1.1 400 Bad Request
min must be a whole number.

Common mistakes.

  • Running go run main.go from another folder, and getting the error about the template. Go into the folder first.
  • Treating an empty min as an error. Then the plain page at / gives status 400.
  • Removing items from allPlayers to filter it. Later requests would then see fewer players, and the handlers would be changing shared data.