Score:1

Custom Menu Template not Rendering Menu Links

ua flag

I'm building a custom module that modifies the main theme (barrio bootstrap in this case) to establish a landing page. In that process, it hides blocks in some regions and takes over rendering the main menu.

When I try to replace the default menu template used by the primary menu, menu--main.html.twig with my own template mymenu--main.html.twig which is exactly the same TWIG code (other than debugging text), it doesn't render the menu links. My custom template is being used, however for some reason it's not rendering the menu links the same.

I'm doing this in a custom module called main_sections which uses the main_sections_theme hook, inside the file main_sections.module, to override the page template with a custom landing page template called, phtml--landing.html.twig.

To render the main menu on this custom page template, I pass the main menu as a TWIG variable called main_menu_def. This seems to work just fine and renders my main menu on my landing page, as expected, and is using the un-modified default menu template from the theme, menu--main.html.twig.

However, when I edit the menu object's #theme variable, to use my custom menu template, mymenu--main.html.twig before passing the variable to the page template, phtml--landing, it no longer renders the menu links.

To illustrate the difference I have passed two menu variables, the first called main_menu_def which is unmodified, and main_menu which is a copy with the theme variable changed to my custom menu template.

main_sections.module

In this file, I have a function, main_sections_block_access that is used to hide unwanted regions, and the theme hook, main_sections_theme in which I pull the main menu object and edit the theme template, as well as register my custom templates, phtml--landing and mymenu--main.

<?php

// use Drupal\block\Controller as bController;
// // use core\modules\block\src\Controller\;
use Drupal\block\Entity\Block;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Updater\Theme;
use Symfony\Component\Console\Completion\Suggestion;



/**
 * Implements hook_block_access().
 * Used to hide blocks on regions not used on pages set by main_sections module
 */
function main_sections_block_access(Block $block, $operation, AccountInterface $account) {
  // get active path
  $current_path = \Drupal::service('path.current')->getPath();
  // $path_alias = \Drupal::service('path_alias.manager')->getAliasByPath($nodeid);
  // \Drupal::service('path.matcher')->matchPath($path_alias, '/landing/*') 

  // path on which to hide blocks
  $hide_on_path = '/landing';
  // if the current path matches hide blocks path
  if ( \Drupal::service('path.matcher')->matchPath($current_path, $hide_on_path) ){
    // list of regions on which to hide blocks
    $hide_regions = array(
      'primary_menu',
      'header',
      'header_form',
      'breadcrumb'
    );
    // Get the block's visibilty condition configuration
    // These are the same visibility settings found on the block config page in the admin UI
    $config = $block->getVisibilityConditions()->getConfiguration();
    foreach ($hide_regions as $region){
      // if block is on a region to hide, then update settings to hide it
      if($block->getRegion() === $region){
        // add the path on which block should be hidden to visibility pages list
        $config['pages'] = $hide_on_path;
        // set visibility pages to 'Hide for listed pages'
        $config['negate'] = TRUE;
        // update block with new visiblity pages config
        $block->setVisibilityConfig('request_path', $config);
      }
    }  //END foreach region
  }  //END path match

}  // END of block access hook


/**
 * Implements hook_theme()
 * Primary module hooks for test_module module.
 */

function main_sections_theme($existing, $type, $theme, $path) {

  $blocks = \Drupal\block\Entity\Block::loadMultiple();
  $block_ids = [];
  $block_regions = [];
  $condition_list = [];
  $visibilities = [];

  foreach($blocks as $block){
    $block_ids[] = $block->id();
    $block_regions[] = $block->getRegion();
    $conditions = $block->getVisibilityConditions()->getConfiguration();
    $visibility = $block->getVisibilityConditions();
    $condition_list[] = $conditions;
    $visibilities[] = $visibility;
  } //for each block


  // $state = \Drupal::state()->getMultiple();

  $theme = \Drupal::config('system.theme')->get('default');

  // get the menu tree interface object
  $menu_tree_interface = \Drupal::menuTree();
  // create menu tree parameters object
  $menu_parameters = new \Drupal\Core\Menu\MenuTreeParameters();
  // get menu tree object for the 'main' navigation menu
  $main_menu_tree = $menu_tree_interface->load('main', $menu_parameters);
  // create array of desired manipulators to apply to menu tree
  $manipulators = array(
    // Only show links that are accessible for the current user.
    array(
      'callable' => 'menu.default_tree_manipulators:checkAccess',
    ),
    // Use the default sorting of menu links.
    array(
      'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
    ),
  );
    // apply manipulators to the menu tree using \Drupal::menuTree()->transform()
    $tree = $menu_tree_interface->transform($main_menu_tree, $manipulators);
    // build transformed menu tree into renderable array object for TWIG
  $main_menu = $menu_tree_interface->build($tree);
  $main_menu_def = $main_menu;
  // $menu_html = \Drupal::service('renderer')->render($main_menu);
  // change to using local theme
  // $main_menu['#theme'] = 'navigation/mymenu--main';
  // $main_menu['#theme'] = 'navigation/mymenu__main';
  // WORKING reference
  $main_menu['#theme'] = 'mymenu__main';
  // $main_menu['#theme'] = 'menu__main';
  
  // get list of site configuration entities
  $site_config = \Drupal::configFactory()->listAll();
  // get array of all system site information configuration parameters
  $site_info = \Drupal::config('system.site')->get();
  // get the site slogan the Drupal system site information configuration
  $site_slogan = \Drupal::config('system.site')->get('slogan');
  // the route for the site slogan is 'system.site_information_settings'
  // $config_main_menu = \Drupal::config('system.menu.main')->get();

  return [
    'pphtml__landing' => [
      // 'render element' => 'children',
      'template' => 'layout/phtml--landing',
      'variables' => [
        'test_var' => 'test text variable',
        'theme' => $theme,
        'menu_theme' => $main_menu['#theme'],
        'path' => $path,
        'type' => $type,
        // 'existing' => $existing,
        // 'main_menu_tree' => $main_menu_tree,
        'main_menu' => $main_menu,
        'main_menu_tree' => $main_menu_tree,
        'main_menu_def' => $main_menu_def,
        // 'site_config' => $site_config,
        // 'site_information' => $site_info,
        'site_slogan' => $site_slogan,
        // 'block_ids' => $block_ids,
        'block_regions' => $block_regions,
        'condition_list' => $condition_list,
        'existing' => $existing,
        // 'visibilites' => $visibilities,
        // 'config_main_menu' => $config_main_menu,
        // 'vars' => $variables,
      ],
      // 'base hook' => 'node',
    ],
    'mymenu__main' => [
      'template' => 'navigation/mymenu--main',
      'variables' => [
        'main_menu' => $main_menu,
        'menus'  => $main_menu_tree,
      ],
      // 'render element' => 'children',
      // 'base hook' => 'menu__main',
      // 'render element' => 'menu',
    ]
  ];
}

phtml--landing.html.twig

Here is the custom landing page template.

<div class="layout-container">
  <header role="banner">
    <div>
      <p>TESTING HEAD</p>
    </div>
    {{ kint() }}
  </header>
  
  <div class="container">
    <div> Menu that automatically uses default bootstrap barrio menu--main twig template </div>
    <nav class="navbar navbar-dark bg-primary navbar-expand-lg  menu--main">
      <div class="container-fluid contextual-region block block-menu navigation justify-content-center">
        {{ main_menu_def }}
      </div>
    </nav>
    <div> Menu with #theme render array variable changed to mymenu--main twig template </div>
    <nav class="navbar navbar-dark bg-primary navbar-expand-lg  menu--main">
      <div class="container-fluid contextual-region block block-menu navigation justify-content-center">
        {{ main_menu }}
      </div>
    </nav>
    <div class="row">
        <p>TESTING BODY</p>
        <div>{{ kint(theme) }}</div>
        <div>{{ kint(menu_theme) }}</div>
        <div>{{ kint(main_menu) }}</div>
        <div>{{ kint(main_menu_tree) }}</div>
    </div>
  </div>

  <footer role="contentinfo">
    {{ page.footer }}
    <div>
      <p>FOOTER TESTING</p>
    </div>
  </footer>
</div>

mymenu--main.html.twig

This is my custom menu template, which is a copy of the default theme's menu template, menu--main with an added text line and kint for debugging.

{% import _self as menus %}
{# {% import _self as main_menu %} #}
{#
  We call a macro which calls itself to render the full tree.
  @see http://twig.sensiolabs.org/doc/tags/macro.html
#}

{{ menus.menu_links(items, attributes, 0) }}
{# {{ main_menu.menu_links(items, attributes, 0) }} #}
<div style="color:lime"> My Main Menu </div>
{{ kint() }}
{{ kint(menus) }}
{{ kint(main_menu) }}
{% macro menu_links(items, attributes, menu_level) %}
  {% import _self as menus %}
  {# {% import _self as main_menu %} #}
  {% if items %}
    {% if menu_level == 0 %}
      <ul{{ attributes.addClass('nav navbar-nav')|without('id') }}>
    {% else %}
      <ul class="dropdown-menu">
    {% endif %}
    {% for item in items %}
      {%
        set classes = [
          menu_level ? 'dropdown-item' : 'nav-item',
          item.is_expanded ? 'menu-item--expanded',
          item.is_collapsed ? 'menu-item--collapsed',
          item.in_active_trail ? 'active',
          item.below ? 'dropdown',
        ]
      %}
      <li{{ item.attributes.addClass(classes) }}>
        {%
          set link_classes = [
            not menu_level ? 'nav-link',
            item.in_active_trail ? 'active',
            item.below ? 'dropdown-toggle',
            item.url.getOption('attributes').class ? item.url.getOption('attributes').class | join(' '),
            'nav-link-' ~ item.url.toString() | clean_class,
          ]
        %}
        {% if item.below %}
          {{ link(item.title, item.url, {'class': link_classes, 'data-bs-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 %}

Resulting Output

The resulting page shows both menus, the top one is using the un-modified menu variable main_menu_def which uses the menu--main barrio bootstrap template, and renders as expected, and the 2nd one using the modified menu variable, main_menu, which uses my custom mymenu--main template which does not render the links. phtml--landing with mymenu--main, full page

Here are the variables of the non-working menu block, expanded and the inspector showing it is pulling the desired template. mymenu--main, expanded

What am I doing wrong with this setup? One of the parts I'm not sure about is how to establish multiple theme template overrides through hooks, properly. Especially as one template override (ie the mymenu--main template) is rendered inside another template override (phtml--landing, in this case). This is what I worked out to get these templates to be recognized and used, but clearly it's not quite right.

leymannx avatar
ne flag
Normally the theme hook contains just the theme definition and you pass the actual values to it when you render something with this `#theme` or alternatively preprocess the actual values into it in a preprocess hook.
leymannx avatar
ne flag
Also the block access hook should return a neutral opinion in the very end.
micbay avatar
ua flag
@leymannx, are you saying I should move my menu building code from the `main_sections_theme` hook into something like, `main_sections_preprocess_pphtml__landing` hook, add pass those menu render array variables by adding them to `$variables[menu_main_def] = $main_menu_def`, for example?
micbay avatar
ua flag
@leymannx sorry, I don't understand what you mean, by, "the block access hook should return a neutral".
Score:0
ua flag

For reasons I don't fully understand, directly changing the #theme parameter of the menu object, in the module's theme hook, as I did above, is not handling the menu object variable the same way as the default theme template. It changes the menu template, but the menu object, and subsequently, the items array variable, are not available at the same level so do not process the same on the template.

In order to have my custom menu handled the same way as the original menu, it was necessary to leave the menu's #theme parameter alone, and instead use a theme_suggestions_HOOK_alter function to add the custom menu template, as such.

main_sections.module file updated from original post (not showing block_access hook, which is still in actual code)

function main_sections_theme_suggestions_menu_alter(array &$suggestions, array $variables) {
  $current_path = \Drupal::service('path.current')->getPath();
  // The landing page menu is given the menu_name, 'landing_menu' inside
  // the main_sections_theme hook so it can be identified here.
  if ($variables['menu_name'] == 'landing_menu') {
    $suggestions[] = 'mymenu__main';
  }
}

/**
 * Implements hook_theme()
 * Primary module hooks for test_module module.
 */
function main_sections_theme($existing, $type, $theme, $path) {
  // get the menu tree interface object
  $menu_tree_interface = \Drupal::menuTree();
  // create menu tree parameters object
  $menu_parameters = new \Drupal\Core\Menu\MenuTreeParameters();
  // get menu tree object for the 'main' navigation menu
  $main_menu_tree = $menu_tree_interface->load('main', $menu_parameters);
  // create array of desired manipulators to apply to menu tree
  $manipulators = array(
    // Only show links that are accessible for the current user.
    array(
      'callable' => 'menu.default_tree_manipulators:checkAccess',
    ),
    // Use the default sorting of menu links.
    array(
      'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
    ),
  );
  // apply manipulators to the menu tree using \Drupal::menuTree()->transform()
  $tree = $menu_tree_interface->transform($main_menu_tree, $manipulators);
  // build transformed menu tree into renderable array object for TWIG
  $main_menu = $menu_tree_interface->build($tree);
  $main_menu_def = $main_menu;

  // Changing the #theme parameter directly does change the template used,
  // However, it does not carry the menu object and variables as desired.
  // $main_menu['#theme'] = 'mymenu__main';
  // Instead it works better to change name of the menu to something that can then be
  // filtered for by main_sections_theme_suggestions_menu_alter,
  // which will generate the association to the desired template.
  $main_menu['#menu_name'] = 'landing_menu';

  return [
    'pphtml__landing' => [
      'template' => 'phtml--landing',
      'path' => \Drupal::service('extension.list.module')->
                getPath('main_sections').'/templates/layout',
      'variables' => [
        'main_menu' => $main_menu,
        'main_menu_def' => $main_menu_def,
      ],
    ],
    // register custom menu template
    'mymenu__main' => [
      'template' => 'mymenu--main',
      'path' => \Drupal::service('extension.list.module')->
                getPath('main_sections').'/templates/navigation',
      // Passing the modified menu variable here is not necessary as it is
      // has already been passed to the 'phtml__landing' template.
      // Here we just need to register our custom templates location.
      // 'variables' => [
      //   'main_menu' => $main_menu,
      // ],
    ]
  ];
}

Resulting Custom Menu Rendering Appropriately

As we can see, the changes above now allow the customized menu to render as expected. However, we also note that the because we used non-conformant TWIG template names, we get an 'INVALID FILE NAME SUGGESTION:' warning in the TWIG debug text in the web inspector. This doesn't seem to cause any functional problems, but to eliminate it we can make further refinements discussed next. enter image description here

main_sections.module, Final Updated Version to Eliminate TWIG Warning

In order to get rid of the template suggestion warning, we can further refine the code to use a template name that follows the expected Drupal patterns. In this case, that can be done by changing the custom template name from mymenu--main.html.twig, to one with the prefix menu--, such as menu--landing.html.twig, and update our template registration appropriately. Using an expected template naming convention that matches our template theme registration name also allows it to be automatically discovered, eliminating the need to specify the template name in the registration.

function main_sections_theme_suggestions_menu_alter(array &$suggestions, array $variables) {
  $current_path = \Drupal::service('path.current')->getPath();
  if ($variables['menu_name'] == 'landing_menu') {
    $suggestions[] = 'menu__landing';
  }
}

/**
 * Implements hook_theme()
 * Primary module hooks for test_module module.
 */
function main_sections_theme($existing, $type, $theme, $path) {
  // get the menu tree interface object
  $menu_tree_interface = \Drupal::menuTree();
  // create menu tree parameters object
  $menu_parameters = new \Drupal\Core\Menu\MenuTreeParameters();
  // get menu tree object for the 'main' navigation menu
  $main_menu_tree = $menu_tree_interface->load('main', $menu_parameters);
  // create array of desired manipulators to apply to menu tree
  $manipulators = array(
    // Only show links that are accessible for the current user.
    array(
      'callable' => 'menu.default_tree_manipulators:checkAccess',
    ),
    // Use the default sorting of menu links.
    array(
      'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
    ),
  );
  // apply manipulators to the menu tree using \Drupal::menuTree()->transform()
  $tree = $menu_tree_interface->transform($main_menu_tree, $manipulators);
  // build transformed menu tree into renderable array object for TWIG
  $main_menu = $menu_tree_interface->build($tree);
  $main_menu_def = $main_menu;

  // Changing menu name to make it filterable by main_sections_theme_suggestions_menu_alter
  $main_menu['#menu_name'] = 'landing_menu';

  return [
    'pphtml__landing' => [
      'template' => 'phtml--landing',
      'path' => \Drupal::service('extension.list.module')->
                getPath('main_sections').'/templates/layout',
      'variables' => [
        'main_menu' => $main_menu,
        'main_menu_def' => $main_menu_def,
      ],
    ],
    // register custom menu template
    'menu__landing' => [
      // No longer necessary to specify template name
      // 'template' => 'menu--landing',
      // The path must still be specified because the template is in a module
      'path' => \Drupal::service('extension.list.module')->
                getPath('main_sections').'/templates/navigation',
    ]
  ];
}

enter image description here

I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.