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 %}
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/bootstrap@5.3.3/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/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>