Multi level menu with TWIG - menu

To generate a simple menu, I can do:
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
and then:
{% embed '...' with { items: ['Home', 'Articles'] %}
But how do I have to write the TWIG code, if I want to create endless deep menus like:
<ul>
<li>Alpha</li>
<li>Bravo</li>
<ul>
<li>Charlie</li>
<li>Delta</li>
<li>Echo</li>
<ul>
<li>Foxtrott</li>
</ul>
<li>Golf</ul>
</ul>
<li>Hotel</li>
<li>India</li>
</ul>
Thx for help!

To perform recursion in twig you can make use of macro's
{% import _self as macro %}
{{ macro.multilevel(foo) }}
{% macro multilevel(array) %}
{% import _self as macro %}
<ul>
{% for item in array %}
<li>
{% if item is iterable %}
{{ macro.multilevel(item) }}
{% else %}
{{ item }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
twigfiddle
EDIT With a simple array it's not possible to nest children in the same <li> as the parent. Herefor you would need to change your array arround,
Reformed array
$data = [
'links' => [
[
'title' => 'alpha',
'href' => 'http://www.alpha.com',
'children' => [],
],
[
'title' => 'beta',
'href' => 'http://www.beta.com',
'children' => [
[
'title' => 'charlie',
'href' => 'http://www.charlie.com',
'children' => [],
],
[
'title' => 'delta',
'href' => 'http://www.delta.com',
'children' => [],
],
[
'title' => 'echo',
'href' => 'http://www.echo.com',
'children' => [
[
'title' => 'foxtrot',
'href' => 'http://www.foxtrot.com',
'children' => [],
],
],
],
],
],
]
];
tiwg
{% macro menu(links) %}
{% import _self as macro %}
<ul>
{% for link in links %}
<li>
{{ link['title'] }}
{% if not link['children']|default([]) is empty %}
{{ macro.menu(link['children']) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
twigfiddle

Related

twig attrbutes.addclass nesting issue

Apologies if this has been asked before, but I'm on my first week working with Drupal and Twig.
I have the following code:
{%
set container_classes = [
'paragraph',
'paragraph--type--' ~ paragraph.bundle|clean_class,
view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class,
not paragraph.isPublished() ? 'paragraph--unpublished',
'container'
]
%}
{% set image_classes = [
'col-12'
]
%}
{% block paragraph %}
{% block content %}
<div{{ attributes.addClass(container_classes) }}>
<div class='row'>
<div{{ attributes.addClass(image_classes) }} data-type='image'>
{{ content.field_two_column_image }}
</div>
<div class='col-12 col-lg-auto' data-type='copy'>
{{ content.field_two_column_copy }}
</div>
</div?>
</div>
{% endblock %}
{% endblock paragraph %}
My issue is the nested attributes.addClass. When I look at the HTML, I'm also seeing the container_classes classes, which is not what I'm looking for.
So how can I separate the two?
You can Create Attributes in Twig.
Something like this should work.
{# attributes for container #}
{% set container_attributes = create_attribute() %}
{%
set container_classes = [
'paragraph',
'paragraph--type--' ~ paragraph.bundle|clean_class,
view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class,
not paragraph.isPublished() ? 'paragraph--unpublished',
'container'
]
%}
{% set container_attributes = container_attributes.addClass(container_classes) %}
{# attributes for image #}
{% set image_attributes = create_attribute() %}
{% set image_classes = [
'col-12'
]
%}
{% set image_attributes = image_attributes.addClass(image_classes) %}
{% block paragraph %}
{% block content %}
<div{{ container_attributes }}>
<div class='row'>
<div{{ image_attributes }} data-type='image'>
{{ content.field_two_column_image }}
</div>
<div class='col-12 col-lg-auto' data-type='copy'>
{{ content.field_two_column_copy }}
</div>
</div?>
</div>
{% endblock %}
{% endblock paragraph %}

Twig For Loop Fails to Iterate Over Nested Schema Properties

I'm new to twig, and I'm trying to create a nav menu with sub menus from a provided schema which looks like this-
menu_items: [
{
url: 'http://testurl.com',
text: 'Menu Item 1',
active: true,
sub_menu: [
{
url: 'http://testurl.com',
text: 'Sub menu Item 1',
},
{
url: 'http://testurl.com',
text: 'Sub menu Item 2',
},
],
},
]
The menu is showing up just fine, but I'm struggling to get the correct syntax to get those sub menu items populated. I put an if statement in because there's not always a sub menu, and then a for loop to create a list item for each sub menu item in the schema, but alas.
{% if menu_items %}
<nav>
<ul>
{% for item in menu_items %}
<li>
{{ item.text }}
{% if item.sub_menu %}
<ul>
{% for sub_menu in item %}
<li>
<a href="{{ item.sub_menu.url }}">
{{ item.sub_menu.text }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
Anyone know where I'm going wrong?
It looks like you are not referencing the sub_menu items correctly. Try
{% for sub in item.sub_menu %} ... {% endfor %}
It's also helpful if the sub_menu array is empty. The empty test evaluates to true if the variable is null, false, an empty array, or an empty string
{% if item.sub_menu is not empty %} ... {% endif %}

Drupal 8 How can I give dropdown links a class?

I'm trying to build a simple dropdown menu on drupal 8 by using twig templates. My problem is that I can't find a way to give the dropdown links a class. Here's my code
{#
/**
* #file
* Theme override to display a menu.
*/
#}
{% import _self as menus %}
{{ menus.menu_links(items, attributes, 0, menu_name) }} {# 1. #}
{% macro menu_links(items, attributes, menu_level, menu_name) %} {# 1. #}
{% import _self as menus %}
{# 1. #}
{%
set menu_classes = [
'c-menu-' ~ menu_name|clean_class,
]
%}
{% if items %}
{% if menu_level == 0 %}
<ul{{ attributes.addClass('navbar-nav u-header__navbar-nav') }}>
{% else %}
<ul class="hs-sub-menu list-unstyled u-header__sub-menu u-header__sub-menu-offset animated">
{% endif %}
{% for item in items %}
{%
set classes = [
menu_level ? 'dropdown-item u-header__sub-menu-list-item' : 'nav-item u-header__nav-item',
item.is_expanded ? 'menu-item',
item.is_collapsed ? 'menu-item',
item.in_active_trail ? 'active',
item.below ? 'nav-item hs-has-sub-menu u-header__nav-item hs-sub-menu-opened',
]
%}
<li{{ item.attributes.addClass(classes) }} data-event="hover" data-animation-in="fadeInUp" data-animation-out="fadeOut">
{%
set link_classes = [
not menu_level ? 'nav-link u-header__nav-link',
item.in_active_trail ? 'active',
item.below ? 'nav-link u-header__nav-link',
item.url.getOption('attributes').class ? item.url.getOption('attributes').class | join(' '),
]
%}
{% if item.below %}
{{ link(item.title, item.url, {'class': link_classes, 'data-toggle': 'dropdown', 'aria-expanded': 'false', 'aria-haspopup': 'true' }) }}
{{ menus.menu_links(item.below, attributes, menu_level + 1) }}
{% else %}
{{ link(item.title, item.url, {'class': link_classes}) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
I just need to give a class to {{ menus.menu_links(item.below, attributes, menu_level + 1) }} completely independent from its parents.
Is there a way to achieve it? My output is like this
Sublink
I need to change it like this
Sublink
If there's any other way to do so like with custom module or with hooks, please let me know
Achieved the result with following code
{#
/**
* #file
* Theme override to display a menu.
*/
#}
{% import _self as menus %}
{{ menus.menu_links(items, attributes, 0, menu_name) }} {# 1. #}
{% macro menu_links(items, attributes, menu_level, menu_name) %} {# 1. #}
{% import _self as menus %}
{# 1. #}
{%
set menu_classes = [
'c-menu-' ~ menu_name|clean_class,
]
%}
{% if items %}
{% if menu_level == 0 %}
<ul{{ attributes.addClass('navbar-nav u-header__navbar-nav') }}>
{% else %}
<ul class="hs-sub-menu list-unstyled u-header__sub-menu u-header__sub-menu-offset animated">
{% endif %}
{% for item in items %}
{%
set classes = [
menu_level ? 'dropdown-item u-header__sub-menu-list-item' : 'nav-item u-header__nav-item',
item.is_expanded ? 'expanded',
item.is_collapsed ? 'collapsed',
item.in_active_trail ? 'active',
item.below ? 'nav-item u-header__nav-item expanded nav-item hs-has-sub-menu u-header__nav-item',
]
%}
<li{{ item.attributes.addClass(classes) }} data-event="hover" data-animation-in="fadeInUp" data-animation-out="fadeOut">
{%
set link_classes = [
'nav-link',
not menu_level ? 'u-header__nav-link' : 'u-header__sub-menu-nav-link',
item.in_active_trail ? 'active',
item.url.getOption('attributes').class ? item.url.getOption('attributes').class | join(' '),
]
%}
{% if item.below %}
{% set title %}
{{ item.title }}<span class="fa fa-angle-down u-header__nav-link-icon"></span>
{% endset %}
{{ link(title, item.url, {'class': link_classes, 'data-toggle': 'dropdown', 'aria-expanded': 'false', 'aria-haspopup': 'true' })}}
{{ menus.menu_links(item.below, attributes, menu_level + 1) }}
{% else %}
{{ link(item.title, item.url, {'class': link_classes}) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}

Is it possible to use for loop in the include tag?

{% include './partial/project.twig' with {'status': 'Past Project',
'statusClass': 'past',
'heroImage': "/dist/images/projects/project-south16th.jpg",
'logo': '/dist/images/logo-south16th.png',
'desc': '23 three- and four-bed townhomes',
'address': '15885 16 Avenue, South Surrey',
'showGallery': true,
'galleryID': 'south16th',
'link': '#',
'galleryImages': "
{% for i in range(1, 10) %}
<a data-fancybox='south16th' href='{{ theme.uri }}/dist/images/gallery/south16/{{ i }}.jpg'></a>
{% endfor %}
"
} %}
The above code is not valid because it seems like twig doesn't allow nested tag in the include tag? Or did I do something wrong?
Is there another way to achieve it? I would like to repeat this code X times and pass it to the template:
{% for i in range(1, 10) %}
<a data-fancybox='south16th' href='{{ theme.uri }}/dist/images/gallery/south16/{{ i }}.jpg'></a>
{% endfor %}
To achieve this you would need to switch up to embed instead of include
index.twig
{% embed 'include.twig' with { 'theme': { 'uri' : 'https://www.example.com', 'pictures': 10, }, } %}
{% block pictures %}
{% for i in 1..theme.pictures %}
<li><a data-fancybox='south16th' href='{{ theme.uri }}/dist/images/gallery/south16/{{ i }}.jpg'></a></li>
{% endfor %}
{% endblock %}
{% endembed %}
include.twig
<h1>Include</h1>
<h2>{{ theme.uri }} - {{ theme.pictures }}</h2>
<ul>
{% block pictures %}
{% endblock %}
</ul>
twigfiddle
note: variables you'd define in include.twig will also be available inside the embed

variables {% set %} in a skeleton file's 'block' don't get recognized , and can't be used when extnding the file?

I have a symfony3/twig skeleton template
page1/skeleton.twig
{# set default values #}
{% block content %}
{% set test = {
sec1: {
title: "null",
content: 'null'
},
}
%}
{% endblock %}
<ul>
19 {% for sec in test[0:] %}
<li>
<p>{{ sec.title }}</p>
<div>
<p>{{ sec.content }}</p>
</div>
</li>
{% endfor %}
</ul>
I then create a layout template that extends the skeleton with 'real' data
page1/layout.html.twig
{% extends 'page1/skeleton.html.twig' %}
{% block content %}
{% set test = {
sec1: {
title: "title1",
content: 'content2'
},
sec2: {
title: "title2",
content: 'content2'
}
%}
{% endblock %}
But when I generate/publish the page, Symfony fires an error
Variable "test" does not exist in :page1:skeleton.html.twig at line 19
500 Internal Server Error - Twig_Error_Runtime
complaining about the skeleton itself.
That 'test' array is defined in the skeleton. Afaict from reading the docs on 'block', 'extends' & 'set', and can't figure out what exactly the problem is.
What do I need to change to eliminate this error?
blocks in twig have their own variable scope.Variables created inside a block can't be accessed outside of it.
Imo you should only test if the variable exist and otherwise create the default value :
skeleton.twig
{% if not test is defined %}
{%
set test = {
sec1: {
title: "null",
content: 'null'
},
}
%}
{% endif %}
<ul>
{% for sec in test[0:] %}
<li>
<p{{ sec.title }}</p>
<div>
<p>{{ sec.content }}</p>
</div>
</li>
{% endfor %}
</ul>
controller.php
<?php
echo $twig->render('page/page.twig', array(
'foo' => [
'title' => 'title1',
'content' => content1',
],
);
Change this in the file page1/skeleton.twig:
{% for sec in test %}
Then it will work.
I tried it. Make sure you understand why!

Resources