1

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.

elithrar
  • 23,364
  • 10
  • 85
  • 104

2 Answers2

2

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.

mjibson
  • 16,852
  • 8
  • 31
  • 42
  • Thanks: I was using a single base template for no other reason than testing purposes. I've split it into `header.tmpl` and `footer.tmpl`, which now works. – elithrar Oct 15 '13 at 06:59
  • Also: it looks like I can't re-define `{{ title }}` across templates. Is there a better way to set the title (in `header.tmpl`) from a content template such as `login_form.tmpl` or `create_listing.tmpl`? – elithrar Oct 15 '13 at 07:05
  • You can `{{template "header.tmpl" .}}` and define a `Title` property in `.`. – mjibson Oct 15 '13 at 07:28
  • you can parse your templates to different template objects and then call those objects. So parse loginForm.tmpl is into a loginFormTemplate *template variable, and add the base template to that template object, then you can call that. – mfhholmes Oct 15 '13 at 13:37
1

This is pretty non-obvious, and I don't know why they do it like this

ParseGlob returns a value that you're throwing away, but you need to keep it; it's the template object that you need to call, so your code should look like:

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

the documentation for the method (as opposed to the library function used above) is a bit unclear, as it says that it associates the templates with the template object that the method is called on, but it also returns a pointer to a template object, which it wouldn't need to do if it worked as advertised. Fun!

mfhholmes
  • 225
  • 2
  • 8
  • Unfortunately this doesn't do the trick; I still get an empty response. – elithrar Oct 15 '13 at 06:50
  • 1
    right *rolls up sleeves* let's have another look... Aha! my bad... you're calling the templates using their filenames, not the template names defined in the files. So instead of passing the template file name to RenderTemplate, pass the actual template name as defined in the `{{define templateName}}` statement – mfhholmes Oct 15 '13 at 13:33