More declarative way to express this view logic?

I have a bit of trouble figuring out how to use Go templates to view a slightly more complex combination of content without resorting to strange constructs.

Context:

  • There are sections document and stuff in the site’s content
  • Each item in stuff relates to a list of documents
    • Has a field documents with the slugs of each related document
  • List of relevant documents $documents

Desired result:

  • A list of all stuff relating to any of the $documents
  • The title should only be visible ithat list is not empty

After fiddling around with this for a while, the solution is to use a counter variable in scratch. Here it goes:

{{ $all_stuff := where .Site.Pages "Section" "stuff" }}
{{ $.Scratch.Set "i" 0 }}
{{ with $all_stuff }}
  {{ range . }}
    {{ if where $documents ".Slug" "in" .Params.documents }}
      {{ if eq ($.Scratch.Get "i") 0 }}
        <h2>Related Stuff</h2>
        <div class="related-documents">
      {{ end }}
      {{ .Render }}
      {{ $.Scratch.Add "i" 1 }}
    {{ end }}
  {{ end }}
  {{ if gt ($.Scratch.Get "i") 0 }}
    </div>
  {{ end }}
{{ end }}

This works but feels kind of wrong to me.

Normally I would reach for map and filter functions, but they do not seem to exist in the templating language. There is only where or in and I really have no idea how to get the same result using them.

Does anybody know of a more declarative way of writing the same logic?

The logic below if fundamentally the same, but it cleans up the use of $all_stuff and the outer with+range usage to simplify the outer loop.

{{ $.Scratch.Set "i" 0 }}
{{ range where .Site.Pages "Section" "stuff" }}
  {{ if where $documents ".Slug" "in" .Params.documents }}
    {{ if eq ($.Scratch.Get "i") 0 }}
      <h2>Related Stuff</h2>
      <div class="related-documents">
    {{ end }}
    {{ .Render }}
    {{ $.Scratch.Add "i" 1 }}
  {{ end }}
{{ end }}

{{ if gt ($.Scratch.Get "i") 0 }}
  </div>
{{ end }}

I’ll have to think a bit about what you’re trying to do. There is now an intersect operator in the latest dev version of Hugo that may be useful here for folding the inner where clause into the outer one.

If you could make up your template functions, how would you solve this conceptually? Or with another template language maybe.

I think the with/range split was an artifact of my failed declarative attempts. Sorry, if that confused matters.

Following your suggestion, let me make up some more functional primitives…

I think it would help if wheres could be nested. intersect comes close, but I do not think it is enough. I would still need something like map, where map ".Foo" $array gives me an array of the .Foo attributes of $array.

For example:

{{
  with where
    (where .Site.Pages "Section" "stuff")
    ".Params.documents" "intersect" (map ".Slug" $documents)
}}
  <h2>Related Stuff</h2>
  <div>
  {{ range . }}
    {{ .Render }}
  {{ end }}
  </div>
{{ end }}

I am beginning to think that Go templates do not compose well. Higher-order functions and lambda expressions might go a long way. But maybe I am also trying to bolt too much functional programming onto an imperative language.

You can already nest where clauses.

Can you use index for the “map” feature? index is a builtin Go template function.

There’s also a slice function in Hugo 0.16-DEV. Don’t think it’s relevant here, though. ?

Untested example:

{{ with where (where .Site.Pages "Section" "stuff") ".Params.documents" "intersect" (index ".Slug" $documents) }}

Reading the documentation, I don’t think index does the same thing as map. It just indexes its argument, not all of its elements and compiling a list of these. By nesting, I was referring to being able to using the inner where as part of a condition, but never mind.