Recursive Loops in Nunjucks

Preamble 🔗

While re-writing my Quizzes page so it's easier to manage, I decided I wanted to separate the HTML markup from the raw data. The problem came when I realized that I needed to recursively generate the HTML so it was made up of nested lists with links.

Thankfully I found a Github issue from someone with the same problem as me. In the issue, there was a response from jbmoelker in which they solved the problem with a macro.

Nunjucks macros 🔗

From the Nunjucks documentation:

macro allows you to define reusable chunks of content. It is similar to a function in a programming language.

Thanks to the reusable quality of macros, you can easily use them to make nested lists recursively.

Setting up the data 🔗

The first step was to set-up the quizzes.json file which contain all the raw data with which to populate the HTML markup. Here's a snippet of that file:

## quizzes.json
[
  {
    "title": "MBTI",
    "children": [
      {
        "title": "16Personalities",
        "url": "https://www.16personalities.com",
        "children": [
          {
            "title": "INFJ-T (Turbulent Advocate)",
            "url": "https://www.16personalities.com/infj-personality"
          }
        ]
      },
      {
        "title": "The Michael Caloz Cognitive Functions Test",
        "url": "http://https://www.michaelcaloz.com/personality/",
        "children": [
          {
            "url": "https://www.michaelcaloz.com/personality/index.html?screen=last&Ti=6&Te=2&Fi=9&Fe=11&Si=4&Se=8&Ni=6&Ne=6&SJ=0&NF=1.5&NT=1.5&SP=0&iFi=0&iTi=0&iSi=1&iNi=0&iFe=0&iTe=1&iSe=0&iNe=1&E=0&I=2&N=2&S=0&T=0&F=2&J=0&P=0",
            "title": "INFJ / INFP"
          }
        ]
      }
    ]
  }
]

As you can see, the basic structure of each object requires a title property, and optionally can include a url property and an object list called children which contains the same recursive object structure.

Applying the Macro 🔗

Now that we have the basic data, we need to generate the nunjucks template in order to get the HTML markup structure correctly.

{% macro quizzItem(quizz) %}
<li>
  {% if quizz.url %}
  <a href="{{ quizz.url }}">{{ quizz.title }}</a>
  {% else %} 
    {{ quizz.title }} 
  {% endif %} 
  
  {% if quizz.children %}
    <ul>
      {% for item in quizz.children %} 
        {{ quizzItem(item) }} 
      {% endfor %}
    </ul>
  {% endif %}
</li>
{% endmacro %} 

In this macro we manage the conditions to print the correct HTML. If the quizz item has
an url property, it puts the title inside an anchor tag. Otherwise, it's a regular text element.

If the quizz item has the children property, it prints a new unordered list and the macro calls itself to generate lists in recursion.

To start running this recursion, I just called it inside a for loop of all quizzes.

<main>
  <ul>
    {% for quizz in quizzes %}
      {{ quizzItem(quizz) }}
    {% endfor %}
  </ul>
</main>

Notes and pitfalls 🔗

The only pitfall I fell into is the fact that you can't declare the macro inside a block declaration.

As said in the documentation:

If you are using the asynchronous API, please be aware that you cannot do anything asynchronous inside macros. This is because macros are called like normal functions. In the future we may have a way to call a function asynchronously. If you do this now, the behavior is undefined.

So in order to fix this, I set up the whole file in these maner:

## /quizzes.html

{% macro quizzItem(quizz) %}
  <li>
    {% if quizz.url %}
      <a href="{{ quizz.url }}">{{ quizz.title }}</a>
    {% else %}
      {{ quizz.title }}
    {% endif %}

    {% if quizz.children %}
      <ul>
        {% for item in quizz.children %}
          {{ quizzItem(item) }}
        {% endfor %}
      </ul>
    {% endif %}
  </li>
{% endmacro %}

<main>
  <ul>
    {% for quizz in quizzes %}
      {{ quizzItem(quizz) }}
    {% endfor %}
  </ul>
</main>

Conclusion 🔗

Thanks to this set-up, I can more easily add data to my new json file and not have to worry about the markup structure, and I can also re-structure the HTML markup easily if I ever want to.

If you want to check the result, you can check my Quizzes page.