Skip to main content

Blog Post

Simplify Menu and Twig Macros

by Tim Dickens
June 28, 2017

It is true that many Frontend Developers loathe the chore of creating a site’s menu, but I personally enjoy building them. I always thought of a website’s menu system as the perfect example of “the evolution of code”.

Most menus start with a simple nested list of menu links; at most, all you need is the text and URL for the menu item. But over time, as the complexities of the menu and site evolve, you begin to watch as a complete custom menu system develops by adding parameters and logic. It’s a beautiful thing to observe.

The same holds true with a menu module. Take the Drupal 8 Simplify Menu module as an example.

Implemented as a TwigExtension to gain access to Drupal’s menu system, the array structure returned from `simplify_menu(menu_machine_name)` includes what is needed to have a simple menu.

"menu_tree": [
  {
    "text": "Section Menu text",
    "url": "#",
    "submenu": []
  }
]

Note that within the `"submenu":` array, the structure within the `{}` can repeat infinitely.

 

Creating a menu macro

With the understanding of the returned array structure, Twig macros can be leveraged to create a recursive menu. Here is an example of a rudimentary menu macro using the structure from the `simplify_menu(menu_machine_name)` function.

{% macro menuMacro(menu) -%}
  <ul>
    {% for menu_item in menu %}
      <li>
        <a href="{{ menu_item.url }}">{{ menu_item.text }}</a>
        {% if menu_item.submenu %}
          {# Since this menu item has a submenu, recall function. #}
          {{ _self.menuMacro(menu_item.submenu) }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{%- endmacro %}

An array of menu items, `menu`, is passed into macro containing `text`, `url`, and `submenu` in each row.

 

Active Trails

Active & active-trail support is not supported in Simplify Menu module at the time of this writing. However, a patch exists on Drupal.org that adds this functionality to the array structure from the Simplify Menu module.

Big shoutout to smurrayatwork for implementing the active trails!

After the successful application of the active trails patch, the array structure returned from `simplify_menu()` includes 2 additional keys per row.

"menu_tree": [
  {
    "text": "Section Menu text",
    "url": "#",
    "active": true|false,
    "active-trail": true|false,
    "submenu": []
  }
]

And with the new keys in the array structure, we can then add some classes to our menu structure.

{% macro menuMacro(menu) -%}
  <ul>
    {% for menu_item in menu %}
      {# Check if this is the active item. #}
      {% set active = (menu_item.active) ? ' is-active' : '' %}

      {# Check if this item is in the active trail. #}
      {% set active = active ~ ((menu_item.active_trail) ? ' is-active-trail' : '') %}

      <li class=”menu__item{{ active }}”>
        <a href="{{ menu_item.url }}"  class=”menu__link{{ active }}”>{{ menu_item.text }}</a>
        {% if menu_item.submenu %}
            {# Since this menu item has a submenu, recall function. #}
          {{ _self.menuMacro(menu_item.submenu) }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{%- endmacro %}

Example uses string concatenation to build string of classes. Optionally, an array could be used with `|join()` in Twig.

 

Menu level classes?

An available class that identifies what level you are in a menu can make many tasks easier. About the easiest way to accomplish this with a Twig macro is to use simple integers. Setting a variable with a default of `1`, the menu would have the ability to know where it is in the loop.

{% macro menuMacro(menu, level) -%}

  {# Set our default level as an integer. #}
  {% set default_level = 1 %}

  <ul class=”menu-level--{{ level|default(default_level) }}”>
    {% for menu_item in menu %}
      {# Check if this is the active item. #}
      {% set active = (menu_item.active) ? ' is-active' : '' %}

      {# Check if this item is in the active trail. #}
      {% set active = active ~ ((menu_item.active_trail) ? ' is-active-trail' : '') %}

      <li class=”menu__item{{ active }}”>
        <a href="{{ menu_item.url }}"  class=”menu__link{{ active }}”>{{ menu_item.text }}</a>
        {% if menu_item.submenu %}
            {# Since this menu item has a submenu, recall function and increment counter. #}
          {{ _self.menuMacro(menu_item.submenu, level|default(default_level) + 1) }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{%- endmacro %}

Parameter `level` is now passed into function, in addition to a new variable added for default value. `level` is then added as part of a class, ensuring that the default value is provided. `level` is then incremented when called recursively.

 

Be careful of over-engineering

While this solution can be used to create a recursive menu, if a recursive menu is not needed, implementing one can be more work than it is worth. If all you are looking in your menu is a few levels at max, it may just be easier to code in the levels in the template, versus creating an intricate recursive structure.

Use the right tool for the job!

Conclusion

At the end of the day, it all comes down to an understanding of recursive functions. A function calling itself recursively will allow an entire array tree to be traversed. Get what you need into that array, or available in another context in the template, and you will be able to act on all levels of the menu structure.

Headshot

Meet team member, Tim Dickens

Tim brings over 5 years of Drupal building and theming experience to his role as a Front End Developer at Mediacurrent. With a keen eye for theming, he is able to look at a design and instantly start to devise various methods that could be used to build the website. He contributes back to the Drupal community as an organizer of NEDCamp and maintains the theme of the organization’s website.

Since the days of his first programming experiences with a computer game called NWN, Tim spent five years teaching himself how to create websites before enrolling at Kaplan University in 2010. He earned an ASIT and a BSIT in Web Development, both acquired Summa Cum Laude with a 4.0 GPA, and now stands as Alumni in the Alpha Beta Kappa Honor Society. When Tim first encountered Drupal 6, it was not love at first sight. However he persevered to overcome Drupal’s learning curve and has now built around 75 - 100 websites, both as a Site-builder and a Themer. Prior to Mediacurrent, Tim held a similar role as a Front-End Web Developer where he spent his days creating stunning responsive websites, create custom JavaScript solutions, learning various programming languages and techniques through hands-on experiences, and making the web awesome through Drupal.

Outside of the Drupal world, Tim lives with his wife and 5 children in Central Virginia. This is naturally in addition to the 2 cats that are ‘his’ masters and his lazy pug. And aside from the normal day-to-day activities of a household of 7, he spends his days tinkering in his garage with woodworking.

Learn more about Tim >

Related Insights