Call other templates with dynamic name
Asked Answered
P

6

20

I do not see a way to call templates (text or html) with a dynamic name. Example:

This works:

{{template "Blah" .}}

This errors with "unexpected "$BlahVar" in template invocation":

{{$BlahVar := "Blah"}}
{{template $BlahVar .}}

The overall problem I'm trying to solve is that I need to render templates conditionally based on a configuration file - so I don't know the names of the templates ahead of time. Obviously I can put a function in the FuncMap which just does a separate template parsing and invocation and returns that result but was wondering if there is a Better Way.

Pukka answered 21/12, 2013 at 7:35 Comment(4)
@Boushley gotcha. See the other answer I just posted which gives the solution I ended up using.Pukka
Got an answer in the meantime?Virga
I am also looking for a way to do it…Padus
I have same trouble. Flimzy just wanted to synamicallt change $BlahVar.Quaky
P
4

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.

Pukka answered 23/3, 2017 at 6:59 Comment(0)
P
24

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)
    }

}
Pukka answered 16/5, 2014 at 22:45 Comment(0)
B
10

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
}
Bobbysoxer answered 21/12, 2013 at 15:12 Comment(1)
Interesting - doesn't particularly solve my problem, but yes this is another approach that could be useful in some cases. But thanks anyway.Pukka
P
4

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.

Pukka answered 23/3, 2017 at 6:59 Comment(0)
F
2

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}}

Fatimafatimah answered 4/12, 2019 at 5:26 Comment(0)
D
0

Faced the same issue while using gin, and simplest solution was:

router.GET("/myurl", func(ctx *gin.Context) {
    /*
       --- do anything here --
    */
    template1 := "template1.html"
    template2 := "template2.html"

    ctx.HTML(200, template1, nil)
    ctx.HTML(200, template1, nil)
})

Basically I split the html content into separated files, and call them up individually. As long as the response code is the same (for example: 200), then it won't be triggering any issue.

Diannediannne answered 8/11, 2021 at 11:17 Comment(0)
K
0

If the template variable selects against a known set of alternatives, then I ended up doing this, either manually or automatically generating a suffix to select between the templates.

{{define "foo [en]"}}
    English version
{{end}}

{{define "foo [cy]"}}
    Welsh version
{{end}}

{{define "foo"}}
    {{if eq .Lang "en"}}
        {{template "foo [en]"}}
    {{else if eq .Lang "cy"}}
        {{template "foo [cy]"}}
    {{else}}
        {{error "missing or invalid .Lang"}}
    {{end}}
{{end}}

Another approach I'm using elsewhere is to, just prior to executing a template, parsing a small dynamically generated template like so:

clone, _ := rootTemplate.Clone() // error checking elided
clone.Parse(`{{define "body"}}{{template "`+name+`" .}}{{end}}`)
clone.ExectueTemplate(...)
Knitter answered 12/12, 2022 at 17:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.