Variables inside templates in golang
Asked Answered
J

3

27

What is the namespace of variables inside html/text templates? I thought that a variable $x can change value inside a template, but this example shows me that I cannot.

I failed when I tried to group tournaments according year - something like this (http://play.golang.org/p/EX1Aut_ULD):

package main

import (
    "fmt"
    "os"
    "text/template"
    "time"
)

func main() {
    tournaments := []struct {
        Place string
        Date  time.Time
    }{
        // for clarity - date is sorted, we don't need sort it again
        {"Town1", time.Date(2015, time.November, 10, 23, 0, 0, 0, time.Local)},
        {"Town2", time.Date(2015, time.October, 10, 23, 0, 0, 0, time.Local)},
        {"Town3", time.Date(2014, time.November, 10, 23, 0, 0, 0, time.Local)},
    }
    t, err := template.New("").Parse(`
{{$prev_year:=0}}
{{range .}}
    {{with .Date}}
        {{$year:=.Year}}
                    {{if ne $year $prev_year}}
                        Actions in year {{$year}}:
                {{$prev_year:=$year}}
            {{end}}
    {{end}}

        {{.Place}}, {{.Date}}
    {{end}}

    `)
    if err != nil {
        panic(err)
    }
    err = t.Execute(os.Stdout, tournaments)
    if err != nil {
        fmt.Println("executing template:", err)
    }
}
Jeuz answered 8/10, 2015 at 22:35 Comment(1)
By writing {{$prev_year:=$year}} you are creating brand new variable.Deepfry
E
20

Edit: See https://mcmap.net/q/507990/-variables-inside-templates-in-golang for a more up-to-date answer.


Original answer:

https://golang.org/pkg/text/template/#hdr-Variables:

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.

So the $prev_year you define with {{$prev_year:=$year}} only lives until.. the next line ({{end}}).

It seems there is no way of going around that.

The "right" way to do this is to take that logic out of your template, and do the grouping in your Go code.

Here is a working example : https://play.golang.org/p/DZoSXo9WQR

package main

import (
    "fmt"
    "os"
    "text/template"
    "time"
)

type Tournament struct {
    Place string
    Date  time.Time
}

type TournamentGroup struct {
    Year        int
    Tournaments []Tournament
}

func groupTournamentsByYear(tournaments []Tournament) []TournamentGroup {
    if len(tournaments) == 0 {
        return nil
    }

    result := []TournamentGroup{
        {
            Year:        tournaments[0].Date.Year(),
            Tournaments: make([]Tournament, 0, 1),
        },
    }

    i := 0
    for _, tournament := range tournaments {
        year := tournament.Date.Year()
        if result[i].Year == year {
            // Add to existing group
            result[i].Tournaments = append(result[i].Tournaments, tournament)
        } else {
            // New group
            result = append(result, TournamentGroup{
                Year: year,
                Tournaments: []Tournament{
                    tournament,
                },
            })
            i++
        }
    }

    return result
}

func main() {
    tournaments := []Tournament{
        // for clarity - date is sorted, we don't need sort it again
        {"Town1", time.Date(2015, time.November, 10, 23, 0, 0, 0, time.Local)},
        {"Town2", time.Date(2015, time.October, 10, 23, 0, 0, 0, time.Local)},
        {"Town3", time.Date(2014, time.November, 10, 23, 0, 0, 0, time.Local)},
    }

    t, err := template.New("").Parse(`
{{$prev_year:=0}}
{{range .}}
    Actions in year {{.Year}}:
    {{range .Tournaments}}

            {{.Place}}, {{.Date}}
    {{end}}
    {{end}}

    `)
    if err != nil {
        panic(err)
    }
    err = t.Execute(os.Stdout, groupTournamentsByYear(tournaments))
    if err != nil {
        fmt.Println("executing template:", err)
    }
}
Eyla answered 9/10, 2015 at 11:25 Comment(4)
Thanks a lot, but this is also problematic. Map I spent some time by thinking and I don't find really idiomatic solution. In this situation, I think, is the best returnJeuz
Thanks a lot, but this is also problematic. Map in template is rendered in ASC oreder, and I need DESC. Therefore I would create a type for year with method Less. Also sorting in my case is not necessary, real data comes from database, therefore is simple to sort that. I spent some time by thinking and I don't find really idiomatic solution. The best to maintain is probably return []struct {PrevYear *int; Tournament}Jeuz
@Jeuz : you're right, a map wasn't the good structure for what you want to do. I added a working example that does the jobEyla
@lofcek, it looks like HectorJ answered your question. Can you accept the answer?Skyscape
S
20

In go1.11 text/template and hence html/template became able to set the value of existing variables, which means that the original code can be made to work with one very small modification.

Change

{{$prev_year:=$year}}

To

{{$prev_year = $year}}

Playground

Scepter answered 22/10, 2018 at 9:7 Comment(1)
{{$prev_year:=0}} before the {{range}} block is key. Variable must be initialized before assignment.Hawkie
S
7

As mentioned by this answer, the scope of that variable "re-assignment" ends with the {{end}} block. Therefore using standard variables only there's no way around the problem and it should be solved inside the Go program executing the template.

In some frameworks however this is not that easy (e.g. protoc-gen-gotemplate).

The Sprig library adds additional functionality to the standard template language. One of them are mutable maps that can be used in the following way:

// init the dictionary (you can init it without initial key/values as well)
{{$myVar := dict "key" "value"}}

// getting the "key" from the dictionary (returns array) and then fetching the first element from that array
{{pluck "key" $myVar | first}}

// conditional update block
{{if eq "some" "some"}}
     // the $_ seems necessary because Go template functions need to return something
     {{$_ := set $myVar "key" "newValue"}}
{{end}}

// print out the updated value
{{pluck "key" $myVar | first}}

This little example prints out:

value
newValue

A pragmatic approach would be to use a single dictionary for all mutable variables and store them under their corresponding variable name as key.

Reference:

Stud answered 21/3, 2018 at 2:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.