Calling a template with several pipeline parameters

不羁的心 提交于 2019-11-26 10:34:53

问题


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

回答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")



回答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




回答3:


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



回答4:


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.




回答5:


The best i've found so far (and I don't really like it) is muxing and demuxing parameters with a "generic" pair container :

http://play.golang.org/p/ri3wMAubPX

type PipelineDecorator struct {
    // The actual pipeline
    Data interface{}
    // Some helper data passed as "second pipeline"
    Deco interface{}
}

func decorate(data interface{}, deco interface{}) *PipelineDecorator {
    return &PipelineDecorator{
        Data: data,
        Deco: deco,
    }
}

I use this trick a lot for building my website, and I wonder if there exist some more idiomatic way to achieve the same.




回答6:


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:


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.




回答8:


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.




回答9:


I implemented a library for this issue which supports pipe-like arguments passing&check.

Demo

{{define "foo"}}
    {{if $args := . | require "arg1" | require "arg2" "int" | args }}
        {{with .Origin }} // Original dot
            {{.Bar}}
            {{$args.arg1}}
        {{ end }}
    {{ end }}
{{ end }}

{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" 42 }}
{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" "42" }} // will raise an error

Github repo



来源:https://stackoverflow.com/questions/18276173/calling-a-template-with-several-pipeline-parameters

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!