Search code examples
gogo-html-template

Go - html/template, template.ParseGlob() and code re-use


I'm trying to get my head around embedding templates using html/template in Go. I very much like the logic-less template design and I have confidence in its ability to safely escape things as expected (sometimes other template libs get this wrong).

I am, however, having a bit of a problem trying to implement a little helper to render my templates in my HTTP handlers based on the "final" template name. My base.tmpl is effectively "standard" across all of my pages, and in cases where it isn't I can set {{ template checkoutJS }} in base.tmpl and add some per-page JS by setting {{ define checkoutJS }}https://path.to/extra.js {{ end }}.

I want to be able to say renderTemplate(w, "template_name.tmpl", data) in my HTTP handlers, where data is a map[string]interface{} containing strings or structs with whatever I wish to fill in.

Here's the code so far:

base.tmpl

{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ template "title" . }}</title>
</head>
<body>
<div id="sidebar">
...
</div>

{{ template "content" }}

<div id="footer">
...
</div>
</body>
</html>

create_listing.tmpl

{{ define "title" }}Create a New Listing{{ end }}

{{ define "content" }}

<form>
  ...
</form>

{{ end }}

login_form.tmpl

{{ define "title" }}Login{{ end }}

{{ define "content" }}

<form>
  ...
</form>

{{ end }}

main.go

package main

import (
  "fmt"
    "github.com/gorilla/mux"
    "html/template"
    "log"
    "net/http"
)

// Template handling shortcuts
var t = template.New("base")

func renderTemplate(w http.ResponseWriter, tmpl string, data map[string]interface{}) {

    err := t.ExecuteTemplate(w, tmpl, data)

  // Things will be more elegant than this: just a placeholder for now!
    if err != nil {
        http.Error(w, "error 500:"+" "+err.Error(), http.StatusInternalServerError)
    }
}

func monitorLoginForm(w http.ResponseWriter, r *http.Request) {

    // Capture forms, etc.

    renderTemplate(w, "login_form.tmpl", nil)
}

func createListingForm(w http.ResponseWriter, r *http.Request) {

    // Grab session, re-populate form if need be, generate CSRF token, etc

    renderTemplate(w, "create_listing.tmpl", nil)
}

func main() {

    r := mux.NewRouter()

    r.HandleFunc("/monitor/login", monitorLoginForm)

    http.Handle("/", r)
    log.Fatal(http.ListenAndServe(":8000", nil))

}

func init() {

    fmt.Println("Starting up.")
    _, err := t.ParseGlob("templates/*.tmpl")
    if err != nil {
        log.Fatal("Error loading templates:" + err.Error())
    }
}

This compiles, but I get back an empty response from my handler. Note that I don't have a route for the second handler: that code is there just to show how I want to call renderTemplate() from handlers.


Solution

  • You cannot do what you want with the current go template package. Templates have no inheritance, and thus no named blocks as you have in your templates. Instead of defining a base template, it is more common to define header and footer templates. Then, in your page templates, explicitly include those where you want them to go.

    Another solution, I believe, would be to have a 2-stage template phase. The first would be to compile templates for all of the blocks of the base template. These get added to the map, and then sent off to the base template for inclusion.