Call other templates with dynamic name

Using htmltemplate.HTML() to inject parsed template("email/test") on another template("email/main")

htmlTplEngine := htmltemplate.New("htmlTplEngine")


_, htmlTplEngineErr := htmlTplEngine.ParseGlob("views/email/*.html")
if nil != htmlTplEngineErr {
    log.Panic(htmlTplEngineErr.Error())
}

var contentBuffer bytes.Buffer
if err := htmlTplEngine.ExecuteTemplate(&contentBuffer, "email/test", params); err != nil {
    return "", "", errors.Wrap(err, "execute content html")
}

var templateBuf bytes.Buffer
if err := htmlTplEngine.ExecuteTemplate(
    &templateBuf,
    "email/main",
    map[string]interface{}{
        "Content": htmltemplate.HTML(contentBuffer.String()),
        "Lang":    language,
    },
); err != nil {
    return "", "", errors.Wrap(err, "execute html template")
}

On "email/main"

{{define "email/main"}}

My email/test template: {{.Content}}

{{end}}


Another way, though perhaps not a better way, would be to have separate template files which all provide the same named template. For example suppose you have a shared layout for a web page:

<html>
  ...
  <body>
    {{template "body" .}}
  </body>
</html>

In each page you do this:

{{define "body"}}
  This will be in the body
{{end}}

And then merge them in code:

func compileTemplate(layout, name string) (*template.Template, error) {
    tpl := template.New(name)
    tpl, err := tpl.ParseFiles(
        "views/layouts/"+layout+".htm",
        "views/"+name+".htm",
    )
    if err != nil {
        return nil, err
    }
    return tpl, nil
}

A different approach that a talented dev I worked with dreamed up was to post-process the Template instance to find any template includes which are not defined and look on the filesystem for a matching file and parse it for each one found; and then render after.

This gives you a setup like follows:

views/index.html:

{{template "/includes/page-wrapper.html" .}}

{{define "body"}}
<div>Page guts go here</div>
{{end}}

{{define "head_section"}}
<title>Title Tag</title>
{{end}}

includes/page-wrapper.html:

<html>
<head>
{{block "head_section" .}}{{end}}
<head>
<body>

{{template "body" .}}

</body>
</html>

And your ServeHTTP() method looks for files in the "views" directory, loads and parses it and then calls TmplIncludeAll() (below).

I ended up adapting this same basic concept as just a couple of functions, which are as follows. t is the template after being parsed but before rendering. And fs is the directory where "views" and "includes" live (referred to above).

func TmplIncludeAll(fs http.FileSystem, t *template.Template) error {

    tlist := t.Templates()
    for _, et := range tlist {
        if et != nil && et.Tree != nil && et.Tree.Root != nil {
            err := TmplIncludeNode(fs, et, et.Tree.Root)
            if err != nil {
                return err
            }
        }
    }

    return nil
}

func TmplIncludeNode(fs http.FileSystem, t *template.Template, node parse.Node) error {

    if node == nil {
        return nil
    }

    switch node := node.(type) {

    case *parse.TemplateNode:
        if node == nil {
            return nil
        }

        // if template is already defined, do nothing
        tlist := t.Templates()
        for _, et := range tlist {
            if node.Name == et.Name() {
                return nil
            }
        }

        t2 := t.New(node.Name)

        f, err := fs.Open(node.Name)
        if err != nil {
            return err
        }
        defer f.Close()

        b, err := ioutil.ReadAll(f)
        if err != nil {
            return err
        }

        _, err = t2.Parse(string(b))
        if err != nil {
            return err
        }

        // start over again, will stop recursing when there are no more templates to include
        return TmplIncludeAll(fs, t)

    case *parse.ListNode:

        if node == nil {
            return nil
        }

        for _, node := range node.Nodes {
            err := TmplIncludeNode(fs, t, node)
            if err != nil {
                return err
            }
        }

    case *parse.IfNode:
        if err := TmplIncludeNode(fs, t, node.BranchNode.List); err != nil {
            return err
        }
        if err := TmplIncludeNode(fs, t, node.BranchNode.ElseList); err != nil {
            return err
        }

    case *parse.RangeNode:
        if err := TmplIncludeNode(fs, t, node.BranchNode.List); err != nil {
            return err
        }
        if err := TmplIncludeNode(fs, t, node.BranchNode.ElseList); err != nil {
            return err
        }

    case *parse.WithNode:
        if err := TmplIncludeNode(fs, t, node.BranchNode.List); err != nil {
            return err
        }
        if err := TmplIncludeNode(fs, t, node.BranchNode.ElseList); err != nil {
            return err
        }

    }

    return nil
}

This is my favorite approach and I've been using this for a while now. It has the advantage that there is only one template render, the error messages are nice and clean and the Go template markup is very readable and obvious. It would be great if the guts of html/template.Template made this simpler to implement, but it overall is an excellent solution IMO.


As a note on this and to follow up, I eventually ended up with two main answers to this question: 1) Try to avoid this. In several cases a simple if statement worked fine. 2) I was able to accomplish this using a function in the FuncMap that just does a separate rendering. It's not the greatest thing in the world, but it does work and solves the problem. Here is a full standalone demo that shows the idea:

package main

import (
    "bytes"
    "html/template"
    "os"
)

func main() {

    var err error

    // our main template here calls a sub template
    tpl := template.New("main")

    // provide a func in the FuncMap which can access tpl to be able to look up templates
    tpl.Funcs(map[string]interface{}{
        "CallTemplate": func(name string, data interface{}) (ret template.HTML, err error) {
            buf := bytes.NewBuffer([]byte{})
            err = tpl.ExecuteTemplate(buf, name, data)
            ret = template.HTML(buf.String())
            return
        },
    })

    // this is the main template
    _, err = tpl.Parse(`

{{$Name := "examplesubtpl"}}

from main template

{{CallTemplate $Name .}}

`)
    if err != nil {
        panic(err)
    }

    // whatever code to dynamically figure out what templates to load

    // a stub just to demonstrate
    _, err = tpl.New("examplesubtpl").Parse(`

this is from examplesubtpl - see, it worked!

`)
    if err != nil {
        panic(err)
    }

    err = tpl.Execute(os.Stdout, map[string]interface{}{})
    if err != nil {
        panic(err)
    }

}