htmltemplatesgogo-html-template

How to create a global variable and change in multiple places in golang html/template?


I am creating a variable in html/template and and changing the value based on a condition. But the scope of the value stays only inside the if condition:

{{if .UserData}}
    {{$currentUserId := .UserData.UserId}}
    [<a href="#ask_question">Inside {{$currentUserId}}</a>]
{{else}}
    {{$currentUserId := 0}}
{{end}}
[<a href="#ask_question">outside {{$currentUserId}}</a>]

Inside the if condition I am getting the correct value but outside it is 0. How can I use the $currentUserIdoutside the condition? Could someone help me with this?


Solution

  • Go 1.11 added support for changing values of template variables. To define a variable, use :=:

    {{$currentUserId := 0}}
    

    To change its value, use assignment =:

    {{$currentUserId = .UserData.UserId}}
    

    If the variable is created outside of the {{if}} block but changed inside it, changes will be visible after the {{if}} block.

    {{$currentUserId := 0 -}}
    Before: {{$currentUserId}}
    {{if .UserData -}}
        {{$currentUserId = .UserData.UserId}}
        [<a href="#ask_question">Inside {{$currentUserId}}</a>]
    {{else}}
        {{$currentUserId = 0}}
    {{end}}
    [<a href="#ask_question">outside {{$currentUserId}}</a>]
    

    Testing this like:

    m := map[string]interface{}{}
    t := template.Must(template.New("").Parse(src))
    
    m["UserData"] = UserData{99}
    if err := t.Execute(os.Stdout, m); err != nil {
        panic(err)
    

    The output is (try it on the Go Playground):

    Before: 0
    
        [<a href="#ask_question">Inside 99</a>]
    
    [<a href="#ask_question">outside 99</a>]
    

    Original answer follows.


    Short answer is: you can't.

    By design philosophy, templates should not contain complex logic. In templates you can only create new variables, you cannot change the values of existing template variables. When you do {{$currentUserId := .UserData.UserId}} inside an {{if}} block, that creates a new variable which shadows the outer one, and its scope extends to the {{end}} action.

    This is described in the text/template package, Variables section (the same applies to the html/template package too):

    A variable's scope extends to the "end" action of the control structure ("if", "with", or "range") in which it is declared, or to the end of the template if there is no such control structure. A template invocation does not inherit variables from the point of its invocation.

    Possible workarounds

    Easiest in your case would be to register a custom function CurrentUserId() to your template, which if UserData is present, returns its id UserData.UserId(), and if it is not present, returns 0 as in your case.

    This is an example how it can be done:

    type UserData struct {
        UserId int
    }
    
    func main() {
        m := map[string]interface{}{}
        t := template.Must(template.New("").Funcs(template.FuncMap{
            "CurrentUserId": func() int {
                if u, ok := m["UserData"]; ok {
                    return u.(UserData).UserId
                }
                return 0
            },
        }).Parse(src))
    
        if err := t.Execute(os.Stdout, m); err != nil {
            panic(err)
        }
        m["UserData"] = UserData{99}
        if err := t.Execute(os.Stdout, m); err != nil {
            panic(err)
        }
    }
    
    const src = `Current user id: {{CurrentUserId}}
    `
    

    It first executes the template without UserData, then it executes the template with UserData having UserId = 99. The output is:

    Current user id: 0
    Current user id: 99
    

    Try it on the Go Playground.

    Simulating changeable variables

    You can also simulate changeable variables, but also with registered custom function(s). You could register a function like SetCurrentUserId() which would change the value of a variable, preferably in the params that is passed to the template execution so it remains safe for concurrent use.

    This is an example how to do it (it uses a map as template data, and SetCurrentUserId() sets the current user id as a value in this map):

    func main() {
        m := map[string]interface{}{}
        t := template.Must(template.New("").Funcs(template.FuncMap{
            "SetCurrentUserId": func(id int) string {
                m["CurrentUserId"] = id
                return ""
            },
        }).Parse(src))
    
        if err := t.Execute(os.Stdout, m); err != nil {
            panic(err)
        }
        m["UserData"] = UserData{99}
        if err := t.Execute(os.Stdout, m); err != nil {
            panic(err)
        }
    }
    
    const src = `Before: {{.CurrentUserId}}
    {{if .UserData}}
        {{SetCurrentUserId .UserData.UserId}}Inside: {{.CurrentUserId}}
    {{else}}
        {{SetCurrentUserId 0}}Inside: {{.CurrentUserId}}
    {{end}}
    After: {{.CurrentUserId}}
    `
    

    This again first executes the template without UserData, then it executes the template with UserData having UserId = 99. The output is:

    Before: 
        Inside: 0
    After: 0
    Before: 0
        Inside: 99
    After: 99
    

    Try it on the Go Playground.