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 withfunction "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 withunexpected 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, <b>Keiko</b>!</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
<.
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.gofrom another folder, and getting the error about the template. Go into the folder first. - Treating an empty
minas an error. Then the plain page at/gives status 400. - Removing items from
allPlayersto filter it. Later requests would then see fewer players, and the handlers would be changing shared data.