-
Notifications
You must be signed in to change notification settings - Fork 18.6k
Description
This proposal is about providing a way to pass a template.FuncMap at template execution overriding specific functions within the template.
When executing a template using one of new methods, then the provided template.FuncMap will be checked first.
Summary
Reasons:
- Passing values to templates can only be achieved using the
data anyparameter and this is suboptimal for contextual rendering elements such as web nonces likecsrf, or translation functions. - This is especially problematic when using sub-templates called from within template rendering. The template calling syntax only allows setting a single variable as its data, making passing this contextual rendering functions difficult.
- The current suggestion is to use
Clone(),Funcs()and thenExecute()but this is slow and optimizing this appears very difficult. (text/template: optimize Clone+Funcs+Execute sequence #38114) It would require significant changes throughout the code and even if parent templates could be changed to allow inheritance, It would requires cloning every template that could be called in advance.
Advantages:
- The proposal is easy to implement (see text/template: add ExecuteFuncMap and ExecuteTemplateFuncMap #54432)
- The additional methods are simple to use and represent a very small API change.
- The change will allow significant simplification of templates in web services.
Detailed rationale
Sometimes data needs to be rendered differently based on the context it is rendered in.
The two most common examples anti-CSRF tokens and locales.
Although currently these can be added to the data struct provided when rendering - these are not strictly data and it significantly complicates rendering especially when considering subtemplating.
Proposed API
I propose that the following methods are added to text/template.Template and to html/template.Template
text/template:
func (t *Template) ExecuteFuncMap(wr io.Writer, data any, funcs FuncMap) error { ... }
func (t *Template) ExecuteTemplateFuncMap(wr io.Writer, name string, data any, funcs FuncMap) error { ... }html/template:
func (t *Template) ExecuteFuncMap(wr io.Writer, data any, funcs FuncMap) error { ... }
func (t *Template) ExecuteTemplateFuncMap(wr io.Writer, name string, data interface{}, funcs FuncMap) error {Calling these methods would execute the template as per Execute and ExecuteTemplate with the only difference being that when a template calls a function the provided funcs template.FuncMap would be checked first. In this way funcs can be considered a map of function overrides for template execution and acts similarly to the calling the Funcs(...) method on the template but would also override functions in templates called by the current executing template.
Examples
Current Status
Let us assume we have a function with the following signature:
func (l *Locale) Tr(key string, args ...any) string { ... }In this example this function takes a key and provides the translated value for it.
Clearly, this cannot be compiled into shared templates as the locale will be different on a per request basis.
So assuming we don't want to have multiple compiled templates per locale we will need to pass this in as data. Let's say as .Tr.
This leads to:
...
{{.Tr "key" args}}
...
But... what about when ranging over a set of items? Now, . will refer to an item in the range of items so we need use $...
{{range .Items}}
...
{{$.Tr "key" .}}
...
{{end}}
If we forget we will get a runtime error. So we won't know that it's a problem until it's run.
But... what if we want to reuse that range block as a subtemplate or even just extract it out because it is becoming unwieldy? Ideally we'd want to do:
{{range .Items}}
{{template "render-item" .}}
{{end}}
That of course doesn't work because we lose access to $. So now we need to do something horrible like:
{{range .Items}}
{{template "render-item" dict "Item" . "Tr" $.Tr}} {{/* or "root" $ */}}
{{end}}
and then in our subtemplate refer to .Item and .Tr (or .root.Tr). This means that we need to now know that we're in a subtemplate and every time we call the subtemplate we need to do a similar hack.
Again if we forget to add the correct prefix we'll get a runtime error but only at time of render.
Why isn't template.Clone() template.Funcs() template.Execute() sufficient?
The problem here is multifactorial, Clone() isn't very lightweight and requires locks, Funcs(...) also requires locks and every template would need to be cloned and its funcs updated in case when you execute the template it calls another template.
The referenced issue has been around for over 2 years and it is likely that improving the efficiency of this route significantly is not possible without significant refactors.
Instead, the proposed API makes for a clear and simple implementation with a minimal changeset.
Proposed
The proposed API allows for much simpler templating. The Tr function would be passed in within an override FuncMap e.g.:
err := tmpl.ExecuteFuncMap(wr, data, template.FuncMap(map[string]any{"Tr": locale.Tr})Then the downstream templates look like:
{{Tr "key" args...}}
{{range .Items}}
{{Tr "key" .}}
{{template "item-template" .}}
{{end}}
And the Tr function is available everywhere without having to determine whether it needs a ., a $ or even a $.root. (Or even if it's not been passed in to this subtemplate.)
Compatibility
The proposed functionality is completely backwards compatible.
Implementation
The proposal is easy to implement and has been implemented as #54432.
Metadata
Metadata
Assignees
Type
Projects
Status