Skip to main content
ocean waves

Blog Post

Simplify Menu and Twig Macros

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.

Thumbnail

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...

Learn more about Tim >