In a Go template range loop, are variables declared outside the loop reset on each iteration?
Asked Answered
R

2

7

I'm trying to use a variable declared outside a Go template range loop to see if the previous post occurred on the same day as the current post. Here's a simplified example.

Where .Posts is an array of post structs that each have a .Content and a .Date.

{{ $prevDate := "" }}
{{ range $post := .Posts }}
    {{ if ne $prevDate $post.Date }}
        <div class="post-date">Posts dated: {{ $post.Date }}</div>
    {{ end }}
    <div class="post-content">{{ $post.Content }}</div>
    {{ $prevDate := $post.Date }}
{{ end }}

The problem is that $prevDate seems to be reset to "" at the start of each iteration of the loop.

Can anyone help me understand why the value of $prevDate is reset on each iteration and perhaps suggest a way to accomplish what I'm trying to do here?

Rights answered 23/2, 2015 at 12:48 Comment(0)
P
13

Note: Go 1.11 will support modifying template variables via assignment. This will be valid code:

{{ $v := "init" }}
{{ if true }}
  {{ $v = "changed" }}
{{ end }}
v: {{ $v }} {{/* "changed" */}}

Original answer pre-dating Go 1.11 follows:


Variables are not reset. Basically what happens is that you redeclare the $prevDate variable inside the loop. But it is only in scope after the redeclaration and before the closing {{end}} tag of the {{range}}. So when the next iteraiton of the loop comes, you only see the "outer" variable which you haven't changed (because you created a new).

You can't change the values of the template variables you create.

What you can do is for example use the following range form:

{{ range $index, $post := .Posts }}

And...

Solution #1: with a registered Function

And you can register a function for the template (see template.Funcs()) to which you can pass the $index and it would return the date field of the previous element (at $index -1).

It would look something like this:

func PrevDate(i int) string {
    if i == 0 {
        return ""
    }
    return posts[i-1].Date
}

// Registering it:
var yourTempl = template.Must(template.New("").
    Funcs(map[string]interface{}{"PrevDate": PrevDate}).
    Parse(yourStringTemplate))

And from your template you can call it like:

{{range $index, $post := .Posts}}
    {{$prevDate := PrevDate $index}}
{{end}}

Solution #2: with a Method of Posts

This solution is analog but is even simpler: add a method to your Posts and you can call it directly. No need to register a function.

For example:

type Post struct {
    // Your Post type
    Date string
}

type Posts []Post

func (p *Posts) PrevDate(i int) string {
    if i == 0 {
        return ""
    }
    return (*p)[i-1].Date
}

And from your template you can call it like:

{{range $index, $post := .Posts}}
    {{$prevDate := $.Posts.PrevDate $index}}
{{end}}
Pinchpenny answered 23/2, 2015 at 13:19 Comment(3)
Thanks for the detailed explanations. As I am very new to Go, I didn't realize (until you pointed it out) that := was actually redeclaring $prevDate. Is there a way to modify the value of $prevDate within the template itself that is declared outside of the range loop?Rights
@DelPutnam No, you can't modify template variables, you can only redeclare them, which is fine in many cases (you can still refer to it by the same name), but you must know that all (re)declarations are scoped, they are in effect from the point of (re)declaration until the end of the current block. The philosophy is that complex things shouldn't be part of the template but should be implemented in Go (and accessed from or passed to the templates). Yes, you could argue that changing the value of a variable isn't complex but it doesn't change the fact that you can't modify them.Pinchpenny
One thing that got me is that declaring a var works via {{$a:=1}}, but it does not work when re-declaring a var via {{a=1}}; a space is required before the = when re-declaring ({{a =1}}).Immediate
S
2

Go templates are not designed to support complex logic. There's the Go programming language for that. Templates have limitations as a consequence of this philosophy. One limitation is that template variables cannot be changed.

One way to handle this limitation is to structure the data in Go to match the structure of output. Create a type to hold posts for a date and render a slice of these types. The template simply ranges through PostsForDate and Posts.

type PostsForDate struct {
    Date time.Time
    Posts []*Post
}

var Dates []PostsForDate

{{range .Dates}}
    <div class="post-date">Posts dated: {{.Date}}</div>
    {{range .Posts}}
       <div class="post-content">{{.Content}}</div>
    {{end}}
{{end}}

A simpler option (that goes against the design philosophy to some degree) is to create a type in Go to record a current value and report changes to that value.

type change struct {
    current interface{}
}

func (c *change) Changed(next interface{}) bool {
    result := c.current != next
    c.current = next
    return result
}

func newChange() *change {
    return &change{&struct{ int }{}} // initial value ensures that first change is fired.
}

and hook it into a template using a template function:

t := template.Must(template.New("").Funcs(template.FuncMap{"change": newChange}).Parse(` some template `))

Use it in a template like this:

{{ $i := change }}
{{ range $post := .Posts }}
    {{ $i.Change $post.Date }}
        <div class="post-date">Posts dated: {{ $post.Date }}</div>
    {{ end }}
    <div class="post-content">{{ $post.Content }}</div>
{{ end }}

playground example

If the post Date field is a time.Time and the posts have different times within a day, then the above does not work as desired. A workaround for this is to check for changes in the rendered date (for example $post.Date.Format "2006-01-02"). Add the following method to simplify this:

func (c *change) ChangedValue(next interface{}) interface{} {
    if c.current != next {
        c.current = next
        return next
    }
    return nil
}

Use it like this:

{{ $i := change }}
{{ range $post := .Posts }}
    {{with $i.ChangedValue ($post.Date.Format "2006-01-02")}}
        <div class="post-date">Posts dated: {{.}}</div>
    {{ end }}
    <div class="post-content">{{ $post.Content }}</div>
{{ end }}

This only works when the values are guaranteed to be considered true by the template package.

This solution does not require parsing the template on every use (as in solution #1 in the other answer) and it applies to arbitrary slice types (unlike both solutions in the other answer).

Sellers answered 18/12, 2017 at 1:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.