Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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).
like image 232
Deleplace Avatar asked Aug 16 '13 14:08

Deleplace


3 Answers

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")
like image 119
tux21b Avatar answered Oct 13 '22 10:10

tux21b


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

like image 27
val Avatar answered Oct 13 '22 09:10

val


The most straightforward method (albeit not the most elegant) - especially for someone relatively new to go - is to use anon structs "on the fly". This was documented/suggested as far back as Andrew Gerrand's excellent 2012 presentation "10 things you probably don't know about go"

https://talks.golang.org/2012/10things.slide#1

Trivial example below :

// define the template

const someTemplate = `insert into {{.Schema}}.{{.Table}} (field1, field2)
values
   {{ range .Rows }}
       ({{.Field1}}, {{.Field2}}),
   {{end}};`

// wrap your values and execute the template

    data := struct {
        Schema string
        Table string
        Rows   []MyCustomType
    }{
        schema,
        table,
        someListOfMyCustomType,
    }

    t, err := template.New("new_tmpl").Parse(someTemplate)
    if err != nil {
        panic(err)
    }

    // working buffer
    buf := &bytes.Buffer{}

    err = t.Execute(buf, data)

Note that this won't technically run as-is, since the template needs some minor cleaning-up (namely getting rid of the comma on the last line of the range loop), but that's fairly trivial. Wrapping the params for your template in an anonymous struct may seem tedious and verbose, but it has the added benefit of explicitly stating exactly what will be used once the template executes. Definitely less tedious than having to define a named struct for every new template you write.

like image 3
Yuri Avatar answered Oct 13 '22 09:10

Yuri