Search code examples
pythonflaskjinja2

Jinja2 (flask): How to pass blocks to a macro?


I'm implementing a python flask webapp and I'm trying to write a macro to which I want to pass three blocks of html-code, but couldn't get it run.

I found a simple example on making use of jinja with fastapi here: https://www.slingacademy.com/article/fastapi-how-to-use-macros-in-jinja-templates/?utm_content=cmp-true I expected that it works, but I couldn't get it run. I tried it and got the error message: "jinja2.exceptions.TemplateAssertionError: block 'content' defined twice".

{% macro panel(title, class='panel') %}
<div class="{{ class }}">
  <h2>{{ title }}</h2>
  {% block content %}{% endblock %}
</div>
{% endmacro %}

{% call(panel, title='My Panel') %}
{% block content %}
  <p>This is a panel content.</p>
{% endblock %}
{% endcall %}

Should this example work or is there a bug in the jinja2 template? Is there an alternative/better way to pass html-code blocks to a macro? I recognized the "caller" approach, but I think it only works with one code block, right?

Thank you!

Kind regards, Jerome

EDIT It looks like the example is to much simplified. I would like to pass 2 or 3 html or even jinja code blocks to the macro. The background is that I want to simplify the use of Bootstrap Accordion which I want to use multiple times in same way. Extending the example it's more like this:

{% macro panel(title, class='panel') %}
<div class="{{ class }}">
  <h2>{{ title }}</h2>
  {% block content1 %}{% endblock %}
   <!-- Do some other stuff in between. -->
  {% block content2 %}{% endblock %}
</div>
{% endmacro %}

{% call(panel, title='My Panel') %}
{% block content1 %}
  <p>This is a panel content.</p>
{% endblock %}
{% block content2 %}
  <p>This is the detailed panel content.</p>
{% endblock %}
{% endcall %}

EDIT 2 Based on the answer and hints of @Detlef, somehow I got the example working with template inheritance, see below. Anyhow, I use the call caller concept as it's the more obvious way to do.

mymacro.j2:

<!doctype html>
<html>
<body>
{% macro panel(title, class='panel') %}
<div class="{{ class }}">
    <h2>{{ title }}</h2>
    {% block content1 %}{% endblock %}
    <h3> Stuff in between. </h3>
    {% block content2 %}{% endblock %}
</div>
{% endmacro %}

{% block macrocall %}{% endblock %}
</body>
</html>

main.j2:

{% extends "mymacro.j2" %}

{% block content1 %}
<p>Panel content1.</p>
{% endblock %}
{% block content2 %}
<p>This is content2.</p>
{% endblock %}

{% block macrocall %}
{{ panel(title='My Panel') }}
{% endblock %}

Solution

  • The command caller() gives you the option of dynamically embedding code in the macro that was called with call(). If you assign a variable name in call() and pass an attribute to caller(), this can also be used with the defined variable name in the embedded code.
    You can find the documentation here.

    {% macro panel(title, class_='panel') %}
    <div class="{{ class_ }}">
        <h2>{{ title }}</h2>
        {{ caller() }}
    </div>
    {% endmacro %}
    
    {% call() panel(title='My Panel') %}
        <p>This is a panel content.</p>
    {% endcall %}
    

    The code passed is for the respective call.

    As long as the block names remain unique within the template, you can also use multiple nested blocks within the macro call.

    {% call() panel(title='My Panel') %}
        {% block content1 %}
        {% endblock %}
        {% block content2 %}
        {% endblock %}
    {% endcall %}
    

    A simple example implementation of a macro for a bootstrap accordion.

    {% macro bs_accordion(items) %}
        <div class="accordion {{ kwargs.class_ }}" id="{{ kwargs.id }}">
            {% for k,v in items.items() -%}
            <div class="accordion-item">
                <h2 class="accordion-header">
                    <button 
                        class="accordion-button {% if not loop.first %}collapsed{% endif %}" 
                        type="button" 
                        data-bs-toggle="collapse" 
                        data-bs-target="#collapse-{{ loop.index }}" 
                        aria-expanded="{{ loop.first | lower }}" 
                        aria-controls="collapse-{{ loop.index }}"
                    >{{ k }}</button>
                </h2>
                <div 
                    id="collapse-{{ loop.index }}" 
                    class="accordion-collapse collapse {% if loop.first %}show{% endif %}" 
                    data-bs-parent="#{{ kwargs.id }}"
                >
                    <div class="accordion-body">
                        {{ caller(v) }}
                    </div>
                </div>
             </div>
            {% endfor -%}
        </div>
    {% endmacro %}
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Example</title>
        <link 
            href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" 
            rel="stylesheet" 
            integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" 
            crossorigin="anonymous">
    </head>
    <body>
        <main class="container my-4">
        {% set items = { 
                'My Panel 1': ['This is the first panel.', 'This is the detailed panel content.'], 
                'My Panel 2': {'Description': 'This is the second panel.', 'Details': 'This is the detailed panel content.'}, 
                'My Panel 3': 'This is the third panel.', 
            } 
        %}
    
        {% call(content) bs_accordion(items, id='accordionExample') %}
            {% if content is string -%}
                {{ content }}
            {% elif content is mapping -%}
                <dl>
                {% for k,v in content.items() -%}
                    <dt>{{ k }}</dt>
                    <dd>{{ v }}</dd>
                    {% if not loop.last -%}
                    <!-- Do some other stuff in between. -->
                    {% endif -%}
                {% endfor -%}
                </dl>
            {% elif content is sequence -%}
                {% for item in content -%}
                    <p>{{ item }}</p>
                    {% if not loop.last -%}
                    <!-- Do some other stuff in between. -->
                    {% endif -%}
                {% endfor -%}
            {% endif -%}
        {% endcall %}
        </main>
    
        <script 
            src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" 
            integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" 
            crossorigin="anonymous"></script>
    </body>
    </html>