Gin: Using setHTMLTemplate in each handler?

Created on 31 May 2015  路  30Comments  路  Source: gin-gonic/gin

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

question

Most helpful comment

@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...

All 30 comments

@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

  1. thanks for mentioning that the 1.0 is out. I wasn't aware of that.
  2. I might be missing something or doing something very wrong, but I don't really understand how 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 above

You 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:

  1. I either need to go with a less optimal solution
  2. give up using extensible templates and duplicate the common parts across all the different pages to overcome the limitation of 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/*

  • common.tmpl.html
  • header.tmpl.html
  • index.tmpl.html
  • login.tmpl.html
  • etc. etc.
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...

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:

  1. Do not use the built-in HTML facilities and do exactly like if you were using http.HandleFunc("/route", handler) instead of gin:

go template.Execute(c.Writer, data)

  1. Or create your own Gin HTML render so you can still keep it very readable using c.HTML()

@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

  1. c.Engine.SetHTMLTemplate is not thread-safe
  2. c.Engine was removed
  3. Gin already provides a non-hacky way to do it.
  4. Parsing the template in each request is so inefficient.

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?
Screenshot from 2020-09-04 06-46-36

Was this page helpful?
0 / 5 - 0 ratings