gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
78.02k stars 7.97k forks source link

Help with nested template #339

Closed zeroactual closed 9 years ago

zeroactual commented 9 years ago

I'm having problems rendering templates. If all the template files are empty except the base template then there is no issue with the way my templates are currently. The problem is that I can't seem to figure out how to render a template with variables across multiple of the included templates such as in the Base layout and inside a navbar or content area.

I tried nested structs but have had no luck figuring out how to do this. Can anyone point me in the right direction?

Base.html

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>{{.title}}</title>
    </head>
    <body>
        {{template "navbar"}}
        {{template "content"}}
    </body>
</html>
{{end}}

Index.html

{{define "index"}}
{{.stuff}}
{{end}}

What I'm trying to do when rendering the template

// Index page template
var index_template = template.Must(template.ParseFiles(
template_dir + "Base.html",
partials_dir + "Navbar.html",
template_dir + "Index.html"))

// Index page handler
r.GET("", func(c *gin.Context) {
    r.SetHTMLTemplate(contact_template)
    c.HTML(200, "base", data)    // How to make data?
})
manucorporat commented 9 years ago

@zeroactual

  1. r.SetHTMLTemplate(contact_template) should be called at initialization. never inside a request since it is not thread safe.
  2. this is not a problem related with Gin
  3. you should pass the parameters to the content template: {{template "content" . }} http://grokbase.com/t/gg/golang-nuts/138ygwm5yf/go-nuts-whats-the-best-way-to-pass-two-variables-to-a-template
manucorporat commented 9 years ago

Btw, to make the data, you can use a map[string]type{ .. } a struct, or use the built-in type gin.H.

c.HTML(200, "base", gin.H{
    "title": "Welcome",
    "stuff": "this is some interesting stuff",
})

gin.H is a map[string]interface{} with some other facilities (it also works with XML) and more convenient since it is much shorter.

zeroactual commented 9 years ago

Okay it seems that switching {{template "content"}} to {{template "content" . }} Fixed the issue with the content not rendering. That only leaves the question of my usage of

r.SetHTMLTemplate(contact_template)

Since it's not thread safe then how do I tell the routes which template I want to render if I have multiple pages. Would I have to change {{template "content" .}} to be {{.content}} then render the partial I wanted to be there and pass the output of the partial in with the data?

manucorporat commented 9 years ago

@zeroactual if you have multiple templates, you have three options:

  1. Change your layout design: instead of a base template + content template, you have content-template that imports the header + footer
  2. Since Gin HTML is pluggable you can use custom renders: you should use multitemplate (created for your use case: https://github.com/gin-gonic/contrib/tree/master/renders/multitemplate
  3. Avoid using c.HTML() and call template.ExecuteTemplate(c.Writer,...) directly.

I recommend you, the second choice!

manucorporat commented 9 years ago

As you can see: c.HTML can execute arbitrary code, it means you can use any setup and or render engine (like pongo2) and continue using the c.HTML() API.

manucorporat commented 9 years ago

Would I have to change {{template "content" .}} to be {{.content}} then render the partial I wanted to be there and pass the output of the partial in with the data?

That could be a different way to fix it! but you would be calling c.HTML(200, "base", gin.H{"content": "index"}). Always "base". which is not nice.

manucorporat commented 9 years ago
templates := multitemplate.New()
templates.AddFromFiles("index",
    template_dir + "Base.html",
    partials_dir + "Navbar.html",
    template_dir + "Index.html")

templates.AddFromFiles("contact",
    template_dir + "Base.html",
    partials_dir + "Navbar.html",
    template_dir + "Contact.html")

router := gin.New()
router.HTMLRender = templates

but I would suggest you to move the HTML templates initialisation to a different method, so the code would be much cleaner:

router := gin.New()
router.HTMLRender = loadTemplates()
zeroactual commented 9 years ago

Okay this multitemplate looks pretty good but it looks like the version go get pulled down won't compile.

# github.com/gin-gonic/contrib/renders/multitemplate
/Users/wes/go/src/github.com/gin-gonic/contrib/renders/multitemplate/multitemplate.go:13: cannot use Render literal (type *Render) as type render.HTMLRender in assignment
/Users/wes/go/src/github.com/gin-gonic/contrib/renders/multitemplate/multitemplate.go:50: undefined: render.HTML
manucorporat commented 9 years ago

go get -u github.com/gin-gonic/gin go get -u github.com/gin-gonic/contrib/renders/multitemplate

? it works for me

zeroactual commented 9 years ago

Updating the dependencies fixed that problem. Right now it's returning a blank string.

What I have now for is

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/contrib/renders/multitemplate"
)

func main() {
    templates := multitemplate.New()
    templates.AddFromFiles("index",
        "Base.html",
        "Navbar.html",
        "Index.html")

    templates.AddFromFiles("contact",
        "Base.html",
        "Navbar.html",
        "Contact.html")

    router := gin.New()
    router.HTMLRender = templates

    router.GET("", func(c *gin.Context) {
        c.HTML(200, "index", gin.H{
            "title": "Home",
            "stuff": "Interesting home stuff",
        })
    })

    router.GET("/contact", func(c *gin.Context) {
        c.HTML(200, "contact", gin.H{
            "title": "Contact",
            "stuff": "Interesting contact stuff",
        })
    })
    router.Run(":8080")
}

Base.html

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{{.title}}</title>
</head>
<body>
{{template "navbar"}}
{{template "content"}}
</body>
</html>
{{end}}

Index.html

{{define "index"}}
<div>
    Main content
    {{.stuff}}
</div>
{{end}}

Contact.html

{{define "contact"}}
<div>
    Contact content
    {{.stuff}}
</div>
{{end}}

**Navbar.html***

{{define "navbar"}}
<div class="nav">This is a navbar</div>
{{end}}

I think that this part is causing me some of the problems

<body>
{{template "navbar"}}
{{template "content"}} <!--- not sure what this should be -->>
</body>
techjanitor commented 9 years ago

I had this same issue with that template design. I basically got rid of the 'base' template and just have a header/footer with all the html boilerplate included with all my other templates. It accomplishes the same thing, and it allows you to use SetHTMLTemplate with ParseGlob for loading templates as well.

For example:

{{define "tag"}}
{{template "header" .}}
Stuff
{{template "footer" .}}
{{end}}

and that can be called with:

c.HTML(200, "tag", data)
manucorporat commented 9 years ago

@zeroactual in your case, remove the {{define "base"}}. Since it is the base, it can not have a defined name...

manucorporat commented 9 years ago

When using multitemplate, Gin runs: templates[name].Execute() instead of template.ExecuteTemplate(name)

manucorporat commented 9 years ago

Also since Base.html is importing a template called: "content" why are you doing {{define "index"}}?

manucorporat commented 9 years ago

@zeroactual @techjanitor fix your templates.

Base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>{{.title}}</title>
</head>
<body>
{{template "navbar"}}
{{template "content" .}}
</body>
</html>

Index.html

{{define "content"}}
<div>
    Main content
    {{.stuff}}
</div>
{{end}}

Contact.html

{{define "content"}}
<div>
    Contact content
    {{.stuff}}
</div>
{{end}}

**Navbar.html***

{{define "navbar"}}
<div class="nav">This is a navbar</div>
{{end}}

RTFM :D This kind of issues are well defined in the standard library documentation.

This is what I changed:

zeroactual commented 9 years ago

Thanks! That fixed it. It seems I got confused with how to do this when I was playing around with only one page and used a glob. The glob only allowed you to have one content block declared so I changed the names.

I was trying to RTM but I'm so used to Java that I think I was looking at this all the wrong way. Thanks again.

dzpt commented 6 years ago

@manucorporat by using multitemplate i have to declare all the multi templates outside of the request handle. It breaks the code organization. Using template.ExecuteTemplate(c.Writer,...) and individual cache system might be the better way.