I am trying to play around with Drupal 8 menu. How I understand now menu is a service, so how to alter menu output in Drupal 8? Is there any documentation? Because googling show no useful results.

What I am trying to achieve is move active class from <a> element to parent <li> element, this should be possible to do in .twig template (I wouldn't like that) so how to get menu in Drupal 8 and alter it's attributes?


I have a complete example for twitter bootstrap, I use:

Note: this code is incomplete, maybe broken with beta update, but it's an example/introduction.

function nature_menu_link(array $variables) {
  $element = $variables['element'];
  $sub_menu = '';

  if ($element['#below']) {
    // Prevent dropdown functions from being added to management menu so it
    // does not affect the navbar module.
    if (($element['#original_link']['menu_name'] == 'management') && (module_exists('navbar'))) {
      $sub_menu = \Drupal::service('renderer')->renderRoot($element['#below']);
    elseif ((!empty($element['#original_link']['depth'])) && ($element['#original_link']['depth'] == 1)) {
      // Add our own wrapper.
      $sub_menu = '<ul class="dropdown-menu">' . \Drupal::service('renderer')->renderRoot($element['#below']) . '</ul>';
      // Generate as standard dropdown.
      $element['#title'] .= ' <span class="caret"></span>';
      $element['#attributes']['class'][] = 'dropdown';
      $element['#localized_options']['html'] = TRUE;

      // Set dropdown trigger element to # to prevent inadvertant page loading
      // when a submenu link is clicked.
      $element['#localized_options']['attributes']['data-target'] = '#';
      $element['#localized_options']['attributes']['class'][] = 'dropdown-toggle';
      $element['#localized_options']['attributes']['data-toggle'] = 'dropdown';
  // On primary navigation menu, class 'active' is not set on active menu item.
  // @see https://drupal.org/node/1896674
  if (($element['#href'] == '<front>' && drupal_is_front_page()) && (empty($element['#localized_options']['language']))) {
    $element['#attributes']['class'][] = 'active';
  $output = l($element['#title'], $element['#href'], $element['#localized_options']);
  return '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";

After I have my view nammed "menu--main.html.twig" :

{#  https://api.drupal.org/api/drupal/core!modules!system!templates!menu.html.twig/8
    menu_name: The machine name of the menu.
    items: A nested list of menu items. Each menu item contains:
      attributes: HTML attributes for the menu item.
      below: The menu item child items.
      title: The menu link title.
      url: The menu link url, instance of \Drupal\Core\Url
      localized_options: Menu link localized options.
<nav class="navbar navbar-default" role="navigation">
  <div class="container-fluid">
    <div class="navbar-header">
      <button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".navbar-collapse">
      <span class="sr-only">Toggle navigation</span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <a class="navbar-brand" href="{{ front_page }}">Site Name : {{ site_name }}</a>

    <div class="collapse navbar-collapse">
      {# All menu and submenu items #}
      <ul class="nav navbar-nav">
        {% for item in items %}
          {# Menu with submenu #}
          {% if item.below %}
            <li class="dropdown">
              <a tabindex="0" data-toggle="dropdown">{{ item.title }} <span class="caret"></span></a>
              <ul class="dropdown-menu" role="menu">
                {# Menu -> Submenu #}
                {% for item in item.below %}
                  {% include "@nature/snippet/menu-infinite-child.html.twig" with { 'item': item } %}
                {% endfor %}
          {# Menu without submenu #}
          {% else %}
            {# Menu - Link #}
            <li><a tabindex="0" href="" {{ item.attributes }}>{{ item.title }}</a></li>
            {# Menu - Separator #}
            {% if loop.last == false %}<li class="divider"></li>{% endif %}
          {% endif %}
        {% endfor %}

      <form class="navbar-form navbar-left" role="search">
        <div class="form-group">
          <input type="text" class="form-control" placeholder="Search">
        <button type="submit" class="btn btn-default">Submit</button>

      <ul class="nav navbar-nav navbar-right">
        <li class="dropdown">
          <a tabindex="0" data-toggle="dropdown">Dropdown 3<span class="caret"></span></a>
          <!-- role="menu": fix moved by arrows (Bootstrap dropdown) -->
          <ul class="dropdown-menu" role="menu">
            <li><a tabindex="0">Action</a></li>
            <li><a tabindex="0">Another action</a></li>
            <li><a tabindex="0">Something else here</a></li>
            <li class="divider"></li>
            <li><a tabindex="0">Separated link</a></li>

This view call "menu-infinite-child.html.twig" for unlimited infinite child (submenu) :

{# Submenu -> Submenu #}
{% if item.below %}
  <li class="dropdown-submenu">
    <a tabindex="0" data-toggle="dropdown">{{ item.title }}</a>
    <ul class="dropdown-menu">
      {% for item in item.below %}
          {% include "@nature/snippet/menu-infinite-child.html.twig" with { 'item': item } %}
      {% endfor %}
{# Submenu without Submenu #}
{% else %}
  <li><a tabindex="0">{{ item.title }}</a></li>
{% endif %}

Note: I use this configuration with bootstrap-submenu http://vsn4ik.github.io/bootstrap-submenu (MIT license).


There is one central problem: the active class is not always added in PHP, but for non-anonymous users its calculated using some javascript, see "active-link.js". For anonymous users, its calculated in \Drupal\system\Controller\SystemController::setLinkActiveClass

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.