unrolled / render

Go package for easily rendering JSON, XML, binary data, and HTML templates responses.
MIT License
1.94k stars 146 forks source link

Template Functions added via HTMLOptions don't seem to work #82

Closed sh4nks closed 3 years ago

sh4nks commented 3 years ago

Hi,

I needed to add a specific function which require the http.Request and found out that I could add them via the HTMLOptions struct. However, it doesn't seem to work for me.

This is what I got:

func (c *AppContext) HTML(w http.ResponseWriter, r *http.Request, status int, tmpl string, data interface{}) {
    csrfField := csrf.TemplateField(r)
    htmlOpts := render.HTMLOptions{
        Funcs: template.FuncMap{
            "csrfField": func() template.HTML {
                return csrfField
            },
            "testFunc": func() string {
                    return "My custom function"
                },
        },
    }
    c.Render.HTML(w, status, tmpl, data, htmlOpts)
}

and in my template I am trying to call it like this:

{{ csrfField }} // also tried with {{ .csrfField }} --> then it returns nothing

It always errors with

panic: template: index:1: function "csrfField" not defined

Upon further investigation I found out that the functions do get registered here:

tpl.Name(): index
opt.Funcs: map[csrfField:0x9141c0 testFunc:0x9141e0]

but don't seem to be available in the template?

I am kinda new to Go so I am not sure if either I am doing something wrong or if this is a bug?

sh4nks commented 3 years ago

I have created a minimal reproducible example here: https://github.com/sh4nks/go-playground

unrolled commented 3 years ago

Hmm, this is interesting. I hadn't thought about using the HTMLOptions for functions... I had only used that for adding in layouts. So this isn't working because the templates are compiled when the app starts up for the first time. This is a nice performance win, but it comes with the trade off not allowing dynamic functions per render. (When compiling the templates it sees the function call but has no reference to it at that point since that function is only defined at the request level) A couple ways you could potentially get around this:

  1. You could set the csrfField value as a data param instead (just like you passed Content in your example project)
  2. You could pass in the request struct to the template, then have some global funcs that act on it (this would be set in the render.Options struct)

If you have other ideas, let me know and we'll see what we can work out.

sh4nks commented 3 years ago

Ah of course -- I hadn't thought about passing the csrfField as a data param. Thanks!

I am just curious - how should the feature added in PR #81 be used?

unrolled commented 3 years ago

If I'm not mistaken, you can define a global template function (inside render.Options) and then override it in the request. Something like this:

func main() {
  rendr := render.New(render.Options{
    Funcs: []template.FuncMap{
      {
        "myFunc": func() string {
          return "My function"
        },
      },
    },
  })

  // ...

  router.Get("/", func(w http.ResponseWriter, r *http.Request) {
    htmlOpts := render.HTMLOptions{
      Funcs: template.FuncMap{
        "myFunc": func() string {
          return "My function from within a request!"
        },
      },
    }
    rendr.HTML(w, 200, "main", map[string]string{"Content": "Test Content"}, htmlOpts)
  })

  // ...

}
sh4nks commented 3 years ago

Yes that works -- thanks for helping out a Go newbie!