gogo-templatesgo-html-template

How can I run a range within a range in golang using html/template


I want to run a range within a range using html/template. The sample code (https://go.dev/play/p/fxx61RwIDhd) looks like this:

package main

import (
    "html/template"
    "os"
)

type Runtest struct {
    ID          int
    SessionType string
}

type Setting struct {
    ID   int
    Type string
}

type templateData struct {
    Runtest []*Runtest
    Setting []*Setting
}

func main() {
    tmpl := template.Must(template.New("nested-range").Parse(`
        {{ range .Runtest }}
            <h1>Runtest ID: {{ .ID }}, Session Type: {{ .SessionType }}</h1>
            <ul>
                {{ range .Setting }}
                    <li>Setting ID: {{ .ID }}, Type: {{ .Type }}</li>
                {{ end }}
            </ul>
        {{ end }}
    `))

    runtestsList := []*Runtest{
        {ID: 1, SessionType: "Type A"},
        {ID: 2, SessionType: "Type B"},
    }

    settingsList := []*Setting{
        {ID: 101, Type: "Setting 1"},
        {ID: 102, Type: "Setting 2"},
        {ID: 201, Type: "Setting X"},
        {ID: 202, Type: "Setting Y"},
    }

    data := templateData{
        Runtest: runtestsList,
        Setting: settingsList,
    }

    err := tmpl.Execute(os.Stdout, data)
    if err != nil {
        panic(err)
    }
}

I get the following error when executing the code:

panic: template: nested-range:5:13: executing "nested-range" at <.Setting>: can't evaluate field Setting in type *main.Runtest

It seems like the template engine sees {{ range .Setting }} as part of {{ range .Runtest }}.

If you try it with just one range the code will work: https://go.dev/play/p/CUOxnBfvAo1

Also one range after the other: https://go.dev/play/p/mge2JsQYOYD

Is it possible to run a range within range using data that is not part of the first range data?


Solution

  • It is a scoping problem. range starts a new scope with the iteration variable being .. That is, when you have

    {{range .Runtest}}
      // Here . points to the current element of the Runtest
    

    You can still refer to the variables in the global scope using $ prefix:

     {{ range .Runtest }}
                <h1>Runtest ID: {{ .ID }}, Session Type: {{ .SessionType }}</h1>
                <ul>
                    {{ range $.Setting }}
    

    Alternatively, you can define loop variables yourself. This is especially useful if you have multiple levels of range and you need to access variables defined in one of the enclosing ones:

    {{ range $key1, $value1 := .someList }}
       {{ range $key2, $value2 := $value1.otherList }}
          // Here, you can access both $value1 and $value2, 
          // as well as the global scope $.GlobalScopeVar