Calling a template with several pipeline parameter

2019-01-07 10:14发布

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

9条回答
我欲成王,谁敢阻挡
2楼-- · 2019-01-07 10:24

Sometimes maps are a quick and easy solution to situations like this, as mentioned in a couple of the other answers. Since you're using Gophers a lot (and since, based on your other question, you care if the current Gopher is an admin), I think it deserves its own struct:

type Gopher struct {
    Name string
    IsCurrent bool
    IsAdmin bool
}

Here's an update to your Playground code: http://play.golang.org/p/NAyZMn9Pep

Obviously it gets a little cumbersome hand-coding the example structs with an extra level of depth, but since in practice they'll be machine-generated, it's straightforward to mark IsCurrent and IsAdmin as needed.

查看更多
戒情不戒烟
3楼-- · 2019-01-07 10:26

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")
查看更多
小情绪 Triste *
4楼-- · 2019-01-07 10:29

based on @tux21b

I have improved the function so it can be used even without specifying the indexes ( just to keep the way go attaches variables to the template)

So now you can do it like this:

{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}

or

{{template "userlist" dict .MostPopular .CurrentUser}}

or

{{template "userlist" dict .MostPopular "Current" .CurrentUser}}

but if the parameter (.CurrentUser.name) is not an array you definitely need to put an index in order to pass this value to the template

{{template "userlist" dict .MostPopular "Name" .CurrentUser.name}}

see my code:

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
    "dict": func(values ...interface{}) (map[string]interface{}, error) {
        if len(values) == 0 {
            return nil, errors.New("invalid dict call")
        }

        dict := make(map[string]interface{})

        for i := 0; i < len(values); i ++ {
            key, isset := values[i].(string)
            if !isset {
                if reflect.TypeOf(values[i]).Kind() == reflect.Map {
                    m := values[i].(map[string]interface{})
                    for i, v := range m {
                        dict[i] = v
                    }
                }else{
                    return nil, errors.New("dict values must be maps")
               }
            }else{
                i++
                if i == len(values) {
                    return nil, errors.New("specify the key for non array values")
                }
                dict[key] = values[i]
            }

        }
        return dict, nil
    },
}).ParseGlob("templates/*.html")
查看更多
等我变得足够好
5楼-- · 2019-01-07 10:38

The way I approach this is to decorate the general pipeline:

type HomeData struct {
    User    Gopher
    Popular []Gopher
    Active  []Gopher
    Recent  []Gopher
}

by creating a context-specific pipeline:

type HomeDataContext struct {
    *HomeData
    I interface{}
}

Allocating the context-specific pipeline is very cheap. You get access to the potentially large HomeData by copying the pointer to it:

t.ExecuteTemplate(os.Stdout, "home", &HomeDataContext{
    HomeData: data,
})

Since HomeData is embedded in HomeDataContext, your template will access it directly (e.g. you can still do .Popular and not .HomeData.Popular). Plus you now have access to a free-form field (.I).

Finally, I make a Using function for HomeDataContext like so.

func (ctx *HomeDataContext) Using(data interface{}) *HomeDataContext {
    c := *ctx // make a copy, so we don't actually alter the original
    c.I = data
    return &c
}

This allows me to keep a state (HomeData) but pass an arbitrary value to the sub-template.

See http://play.golang.org/p/8tJz2qYHbZ.

查看更多
聊天终结者
6楼-- · 2019-01-07 10:41

Ad "... looks like calling a function with only one parameter.":

In a sense, every function takes one paramater - a multivalued invocation record. With templates it's the same, that "invocation" record can be a primitive value, or a multivalued {map,struct,array,slice}. The template can select which {key,field,index} it'll use from the "single" pipeline parameter in whatever place.

IOW, one is enough in this case.

查看更多
霸刀☆藐视天下
7楼-- · 2019-01-07 10:41

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.

查看更多
登录 后发表回答