Calling a template with several pipeline parameters
In a Go template, sometimes the way to pass the right data to the right template feels awkward to me. Calling a template with a pipeline parameter looks like calling a function with only one parameter.
Let's say I have a site for Gophers about Gophers. It has a home page main template, and a utility template to print a list of Gophers.
http://play.golang.org/p/Jivy_WPh16
Output :
*The great GopherBook* (logged in as Dewey)
[Most popular]
>> Huey
>> Dewey
>> Louie
[Most active]
>> Huey
>> Louie
[Most recent]
>> Louie
Now I want to add a bit of context in the subtemplate : format the name "Dewey" differently inside the list because it's the name of the currently logged user. But I can't pass the name directly because there is only one possible "dot" argument pipeline! What can I do?
- Obviously I can copy-paste the subtemplate code into the main template (I don't want to because it drops all the interest of having a subtemplate).
- Or I can juggle with some kind of global variables with accessors (I don't want to either).
- Or I can create a new specific struct type for each template parameter list (not great).
Solution 1:
You could register a "dict" function in your templates that you can use to pass multiple values to a template call. The call itself would then look like that:
{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}
The code for the little "dict" helper, including registering it as a template func is here:
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i+=2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
}).ParseGlob("templates/*.html")
Solution 2:
You can define functions in your template, and have these functions being closures defined on your data like this:
template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User);},}
Then, you can simply call this function in your template:
{{define "sub"}}
{{range $y := .}}>> {{if isUser $y}}!!{{$y}}!!{{else}}{{$y}}{{end}}
{{end}}
{{end}}
This updated version on the playground outputs pretty !!
around the current user:
*The great GopherBook* (logged in as Dewey)
[Most popular]
>> Huey
>> !!Dewey!!
>> Louie
[Most active]
>> Huey
>> Louie
[Most recent]
>> Louie
EDIT
Since you can override functions when calling Funcs
, you can actually pre-populate the template functions when compiling your template, and update them with your actual closure like this:
var defaultfuncs = map[string]interface{} {
"isUser": func(g Gopher) bool { return false;},
}
func init() {
// Default value returns `false` (only need the correct type)
t = template.New("home").Funcs(defaultfuncs)
t, _ = t.Parse(subtmpl)
t, _ = t.Parse(hometmpl)
}
func main() {
// When actually serving, we update the closure:
data := &HomeData{
User: "Dewey",
Popular: []Gopher{"Huey", "Dewey", "Louie"},
Active: []Gopher{"Huey", "Louie"},
Recent: []Gopher{"Louie"},
}
t.Funcs(template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User); },})
t.ExecuteTemplate(os.Stdout, "home", data)
}
Although I am not sure how that plays when several goroutines try to access the same template...
The working example