A Twig extension for succinctly traversing nested lists (e.g. navigation menus). Based on https://github.com/jordanlev/twig-tree-tag, adapted for PHP 8 and Twig 3 by Tac Tacelosky.
Requires PHP 8.1 or higher
composer require tacman/twig-tree-tag
Now register it in services.yaml
# services.yaml
services:
twig.tree:
class: JordanLev\TwigTreeTag\Twig\Extension\TreeExtension
tags:
- { name: twig.extension }
The {% tree %}
tag works almost like {% for %}
, but inside a {% tree %}
you can call {% subtree var %}
to
recursively run your {% tree %}
block with the given var
. The primary use-case for this tag is nested navigation menus.
This extension was written by Alain Tiemblo, (with a few very minor changes by Jordan Lev).
In this example, menu
is an array of objects, each containing name
, url
, and children
properties (children
is itself an array of objects with the same properties, etc).
{% tree item in menu %}
{% if treeloop.first %}<ul>{% endif %}
<li>
<a href="{{ item.url }}">{{ item.name }}</a>
{% subtree item.children %}
</li>
{% if treeloop.last %}</ul>{% endif %}
{% endtree %}
Just like a {% for %}
loop, you can access the key of each list item:
{% tree key, item in menu %}
<li>
<b>Item {{ key }}</b>: {{ item.name }}
{% subtree item.children %}
</li>
{% endtree %}
See the demo directory for more examples
The treeloop
var serves the same purpose inside a {% tree %}
tag as the loop
var does inside a {% for %}
tag. It is named differently so that you can still use loop
when you have a {% for %}
tag inside your {% tree %}
tag (otherwise they would conflict).
treeloop
contains all the same special variables as loop
:
treeloop.index
: The current iteration of the loop within the current nesting level. (1 indexed)treeloop.index0
: The current iteration of the loop within the current nesting level. (0 indexed)treeloop.revindex
: The number of iterations from the end of the loop within the current nesting level (1 indexed)treeloop.revindex0
: The number of iterations from the end of the loop within the current nesting level (0 indexed)treeloop.first
: True if first iteration of the current nesting leveltreeloop.last
: True if last iteration of the current nesting leveltreeloop.length
: The number of items in the sequence of the current nesting leveltreeloop.parent
: The context of the parent nesting level (or the parent context of thetree
tag itself if currently at the root level of the tree).
Additionally, treeloop
also contains 2 extra variables that tell you about the current nesting level:
level
: The current nesting level (1 indexed -- so root level of the tree is 1, 2nd-level is 2, etc)level0
: The current nesting level (0 indexed -- so root level of the tree is 0, 2nd level is 1, etc)
To handle the edge case where you want to start a new tree inside another tree (that is, a new tree "root" with its own markup), use as
in your {% tree %}
tag to assign each tree to a var name, then pass it into subtree
via with
. This allows Twig to know which {% tree %}
should be called when it comes across the {% subtree %}
tag. For example...
{% tree item in menu as treeA %}
{% if treeloop.first %}<ul>{% endif %}
<li>
{{ item.name }}
{% subtree item.children with treeA %}
<h2>Some other tree (that has its own "root", not a sub-tree of treeA):</h2>
{% tree otherthing in item.otherthings as treeB %}
{{ otherthings.name }}
{% subtree otherthings.subitems with treeB %}
{# We use "with treeB" above so Twig knows which parent tree tag to call #}
{% endtree %}
</li>
{% if treeloop.last %}</ul>{% endif %}
{% endtree %}
The MIT License (MIT)
Please read the LICENSE file for more details.