Calling a template with several pipeline parameters
Asked Answered
P

11

49

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).
Preferment answered 16/8, 2013 at 14:51 Comment(0)
S
81

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")
Systaltic answered 16/8, 2013 at 15:29 Comment(5)
This is nice, thank you. So it's a multiple key/value muxer, like PipelineDecorator (see other answer) but with more possible values in one call.Preferment
Before I started to write this code I thought it would become really ugly, but now I kinda like it myself and I am probably going to use it in some of my projects.Systaltic
I mark it as accepted, for it is imho the "state of art" for pipelining arbitrary data (but still a workaround regarding the go templating design choices). Here is my whole example reimplemented with dict() : play.golang.org/p/oWdPlyWfvGPreferment
Are you able to call functions attached to values passed with dict ? I have the problem currently. Since the value become an interface{}, it seems it's not possible anymore to address function attached to it.Pair
with sprig you have this function defined: masterminds.github.io/sprig/dicts.htmlFoist
D
5

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

Dissociation answered 16/8, 2013 at 16:27 Comment(5)
Okay, so in the scope of current template execution we define some "global accessors" to interesting data. The main drawback imho is that I cannot reuse the compiled template, I'm forced to create the FuncMap with closures, then compile, then execute the template for each request.Preferment
Yes, I think in your case, tux21b's solution might be more flexible. Eventually, you just have to pack all your values one way or another...Dissociation
Woh it's good to know one can actually bind new closures after template compilation, I was not aware of this. For the potential race condition, indeed I tried it and used runtime.GoSched() to force goroutine interleaving, and exposed the flaw with panic() : play.golang.org/p/b5WVlUlGjSPreferment
+1 because I stole your answer in Request context in a Go templatePreferment
The thread-safe issue can be addressed by cloning the template before use. This has a cost though.Preferment
S
3

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.

Sukey answered 29/7, 2018 at 23:16 Comment(0)
B
3

Depending on your goals, https://github.com/josharian/tstruct (blog post) might be helpful. You would define a Go struct called UserList, use tstruct to autogenerate FuncMap helpers for it, and then write something like:

{{ template "userlist" UserList (Users .MostPopular) (Current .CurrentUser) }}
Bantu answered 25/6, 2022 at 18:38 Comment(0)
H
1

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

Heck answered 16/8, 2016 at 7:19 Comment(0)
F
1

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")
Fatidic answered 13/9, 2017 at 15:49 Comment(0)
P
0

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.

Preferment answered 16/8, 2013 at 14:55 Comment(0)
F
0

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.

Footnote answered 17/8, 2013 at 11:31 Comment(1)
Precisely : my concern is not about feasibility, it's rather about clean design. Look at other languages (C, Java) that don't have multiple return values : it's akward to define an euclidean division yielding a quotient and a remainder. Imagine a language where functions have only one parameter, and you must use 4 or 5 lines to declare a new data structure before each parameter list : would not feel nice to code.Preferment
D
0

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.

Dugan answered 19/8, 2013 at 7:22 Comment(0)
P
0

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.

Polished answered 30/9, 2014 at 20:17 Comment(4)
Decorating and embedding are promising ideas, but I specifically want the context to be accessible by the subtemplates. Could you address this issue by editing the original example? Thanks!Preferment
Unfortunately your first comment is entirely accurate: "Calling a template with a pipeline parameter looks like calling a function with only one parameter." I have had the best success with keeping the overall "Context" object and mutating one field on it.Polished
OK so it looks like PipelineDecorator (see other answer). And it's a good idea to have Using as a method, instead of a function.Preferment
Interesting, you are correct. The only real difference is embedding the Data in the PipelineDecorator instead of having it as a named field.Polished
S
-1

You can pass all data to it just by . dot.

{{ template "some" . }}
Sinistrocular answered 15/8, 2023 at 15:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.