Hi,
I'm wondering if using gin.Context.Engine.setHTMLTemplate
in each handler is the correct/safe way to set up templates? (_nb_: templates that are trying to reuse layout elements and thus using define
cannot override the define
blocks so for each handler one needs to load only the specific templates).
@al3xandru Context.Engine was removed in Gin v1.0, I recommend you to update as soon as possible. It features tons of performance, security and stability improvements.
Calling setHTMLTemplate from a request is an extremely bad idea since SetHTMLTemplate is not thread safe.
You should initialise all your templates at the beginning:
tmpl := template.Must(template.ParseFiles(r.Files...))
tmpl.ParseFiles("template1.html", "template2.html"...)
router.SetHTMLTemplate(tmpl)
or using:
//router.LoadHTMLFiles("template1.html", "template2.html")
router.LoadHTMLGlob("resources/*)
then just use:
func handler(c *gin.Context) {
c.HTML(200, "template1.html", data)
}
use whatever you want, but never change router configuration after starting the server.
@manucorporat
router.setHTMLTemplate
would work.Given:
base.html
: contains the layout (using define
and template
)page1.html
: contains the "components" for a specific page (basically fills the defines
that are used in base.html for template
)page2.html
: same as aboveYou cannot load all these templates at once as they contain duplicated define
blocks.
What am I doing wrong?
_Update_: I have forgotten to reference #219 and SHA: bee03fa7b0c9bf88ac211f
@al3xandru
router.SetHTMLTemplate() connects your template with c.HTML()
so, you just have to initialize your template and give it to gin.
templates that are trying to reuse layout elements and thus using define cannot override the define blocks so for each handler one needs to load only the specific templates).
you do not have to use SetHTMLTemplate(), you can template.Execute(c.Writer, data)
.
but I do not recommend it, you should architect your website to use a single template object.
https://golang.org/doc/articles/wiki/
There is an inefficiency in this code: renderTemplate calls ParseFiles every time a page is rendered. A better approach would be to call ParseFiles once at program initialization, parsing all templates into a single *Template. Then we can use the ExecuteTemplate method to render a specific template.
maybe I have not understood you.
@manucorporat if I'm reading your answer correctly:
html/template
(_Note_: I know this can lead to debates, but I really cannot see how a decently sized project can actually use a single template for all the pages. Just consider as an example a set of pages for account management, content listing, single content page, content creation, etc.)
While I haven't dug into this too far, having an addHTMLTemplate(name string, t *template.Template)
would be a much more elegant (and easier to maintain) solution.
@al3xandru no, you do not have to duplicate it. One Go template can have internally tons of templates. it is like a tree.
{{ $title := "Page Title" }}
{{ template "head" $title }}
{{ template "checkout" }}
{{ template "top" }}
// I have ~3 different sidebar layouts that change according to the page
// front page, listing creation, payment page - either with some marketing
// fluff or a quick "FAQ" for the user. This is just one of the combinations.
{{ template "sidebar_details" . }}
{{ template "sidebar_payments" }}
{{ template "sidebar_bottom" }}
<div class="bordered-content">
...
{{ template "listing_content" . }}
...
</div>
{{ template "footer"}}
{{ template "bottom" }}
router.LoadHTMLGlob("resources/templates/*")
resources/templates/*
func index(c *gin.Context) {
c.HTML(200, "index.tmpl.html", data)
}
func index(c *gin.Context) {
c.HTML(200, "login.tmpl.html", data)
}
//....
fast, clean and idiomatic.
one *template.Template
means: store all your templates under the same namespace
While I haven't dug into this too far, having an addHTMLTemplate(name string, t *template.Template) would be a much more elegant (and easier to maintain) solution.
that is already built in *template.Template
@manucorporat this only works if there's one and only one 1 definition for each "template"
Please do take a look at https://elithrar.github.io/article/approximating-html-template-inheritance/ and http://www.reddit.com/r/golang/comments/27ls5a/including_htmltemplate_snippets_is_there_a_better/
Meanwhile I'll try to see if the above solution it's something that I can actually do on my side.
@al3xandru ok, I have never had problems with the current design.
Page imports header, footer...
and you want:
Base imports Page
You could create your own HTML render using a map[string]*template.Template.
Let me 10 minutes to write something for you...
@al3xandru
package main
import (
"html/template"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
)
func main() {
router := gin.Default()
router.HTMLRender = createMyRender()
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index", data)
})
router.Run(":8080")
}
type MyHTMLRender struct {
templates map[string]*template.Template
}
func (r *MyHTMLRender) Add(name string, tmpl *template.Template) {
if r.templates == nil {
r.templates = make(map[string]*template.Template)
}
r.templates[name] = tmpl
}
func (r *MyHTMLRender) Instance(name string, data interface{}) render.Render {
return render.HTML{
Template: r.templates[name],
Data: data,
}
}
func createMyRender() render.HTMLRender {
r := &MyHTMLRender{}
r.Add("index", template.Must(template.ParseGlob(pattern)))
r.Add("login", template.Must(template.ParseGlob(pattern1)))
r.Add("something", template.Must(template.ParseGlob(pattern2)))
r.Add("another", template.Must(template.ParseGlob(pattern3)))
return r
}
do you understand? you can create your own render and still use the c.HTML() syntax which is really nice...
render.HTML is called by c.Render()
https://github.com/gin-gonic/gin/blob/master/render/html.go#L60-L67
https://github.com/gin-gonic/gin/blob/master/context.go#L285-L299
and your HTML is rendered!
Thanks for both explaining the current approach and also for the new option. As of this conversation I'm trying to get the default option to work.
@al3xandru in my short experience with "templates" I have seen that most of the people try use one instance of *template.Template with multiple templates inside and then they call:
template.ExecuteTemplate(w, "template3", data)
so, I tried to accommodate the API to the most common use case, but again, I encourage you to keep your current design if it fits better with your requirements.
As I said: you have two options:
http.HandleFunc("/route", handler)
instead of gin: go
template.Execute(c.Writer, data)
@al3xandru I just added the render to the contrib repo:
https://github.com/gin-gonic/contrib/blob/master/renders/multitemplate/multitemplate.go
package main
import (
"html/template"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/contrib/renders/multitemplate"
)
func main() {
router := gin.Default()
router.HTMLRender = createMyRender()
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index", data)
})
router.Run(":8080")
}
func createMyRender() multitemplate.Render {
r := multitemplate.New()
r.Add("index", template.Must(template.ParseGlob(pattern)))
r.AddFromGlob("login", "/resources/login/*")
r.AddFromFiles("something", "template1.html", "template2.html", "template3.html")
return r
}
Hey, just starting out but I thought why not create a middleware solution, thoughts?
package layout
import (
"fmt"
"github.com/gin-gonic/gin"
"html/template"
"net/http"
)
type GinLayout struct {
Status int64
Base string
Template string
Data gin.H
}
func Layout() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
layout := c.MustGet("layout").(GinLayout)
var baseTemplate = fmt.Sprint("views/layouts/", layout.Base)
c.Engine.SetHTMLTemplate(template.Must(template.ParseFiles(baseTemplate, layout.Template)))
c.HTML(http.StatusOK, layout.Base, layout.Data)
}
}
and then in the controller call
func main() {
router := gin.Default()
router.Use(layout.Layout())
router.GET("/", func(c *gin.Context) {
c.Set("layout", layout.GinLayout{
200,
"base.html",
"views/dashboard.html",
gin.H{
"hello": "world",
},
})
})
router.Run(":8080")
}
@chonthu
If you do not want to use the template system provided by Gin: then create something like:
func Layout() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
layout := c.MustGet("layout").(GinLayout)
var baseTemplate = fmt.Sprint("views/layouts/", layout.Base)
tmpl := template.Must(template.ParseFiles(baseTemplate, layout.Template)))
tmpl.ExecuteTemplate(c.Writer, layout.Base, layout.Data)
}
}
^^that solution is slow and hacky but at least it is race condition free
If you need multiple templates, I recommend you the multitemplate render:
https://github.com/gin-gonic/contrib/blob/master/renders/multitemplate/multitemplate.go
but my suggestion is: try to fit your design in just one template.Template.
So instead of having a base.html and a page.html, you have: header, footer, page1, page2...
page1, page2 includes the header, footer... instead of a base templates that includes the content.
This an example of multitemplate, it probably fits very well with your current multi-template setup:
package main
import (
"html/template"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/contrib/renders/multitemplate"
)
func main() {
router := gin.Default()
router.HTMLRender = createMyRender()
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index", data)
})
router.Run(":8080")
}
func createMyRender() multitemplate.Render {
r := multitemplate.New()
r.AddFromFiles("index", "base.html", "base.html")
r.AddFromFiles("article", "base.html", "article.html")
r.AddFromFiles("login", "base.html", "login.html")
r.AddFromFiles("dashboard", "base.html", "dashboard.html")
return r
}
@manucorporat , I use pongo2
, and how about this solution's performance:
func Render(c *gin.Context, pth ...string) {
template := pongo2.Must(pongo2.FromCache("tpl/" + strings.Join(pth, "/") + ".html"))
if err := template.ExecuteWriter(c.Keys, c.Writer); err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
}
}
Then call likes this Render(c, "path", "to", "template")
in a handler
@leedstyh well, a render could also be created for pongo2. so you still use c.HTML()
So you could do:
router := gin.New()
router.HTMLRender = newPongo2Render("filesglob/*")
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index", data) // c.HTML uses pongo2
})
Thanks @manucorporat, I only need one base template. So what i'm going to do is switch to including header and footer templates like your suggestion.
@chonthu if you use the one template design:
you are already setup with this:
router := gin.New()
router.LoadHTMLGlob("templates/*") // this will load all the template files inside templates
I changed all the files to use {{ template "header.html" }} etc. and yup, it worked fine.
I got accustomed to the base approach from other frameworks in other languages. I think the reason for going that direction is to change the base template structure one file and not have to go into each file that has a header and footer.
Its a small disadvantage when it comes to cleanup or big changes but not killer.
@chonthu remember! if you still want to use the base.html setup, then the multitemplate package is a good start point!
Glad I could help you
@manucorporat OK, I'll try to write a new one!
@manucorporat Based on this, if I wanted the ability to load different themes (templates) from an object store like Cloudfiles based on the specific user. I'd need to overwrite HTMLRender
with the logic to pull the files from the object store. Would something like this be possible or am I asking for trouble?
Hi @manucorporat , I am trying to do this (in the screenshot) and its working really well but I dont know if this is a good solution or not?
Most helpful comment
@al3xandru
do you understand? you can create your own render and still use the c.HTML() syntax which is really nice...