Skip to main content

Drupal 8: Theming menus

Submitted by kaa4ever on Wed, 08/05/2015 - 09:17

Drupal 8 ships with some pretty cool new features. One example being that it’s now possible to put the same block into two or more regions in the Block Layout page. That’s very nice, and makes the default block system way more useful than former versions.

Drupal 8 block layout

In the screenshot above, the Main navigation is placed in the Header and Sidebar regions.
Drupal 8 also adds some block instance specific settings. For every block inserted into the layout, it’s now possible to change some custom settings (caching, title, etc) for that specific instance.
For menus you can specific Initial starting level and Maximum number of levels to display. Cool! These two new features might make the need for third party modules like Menu block superfluous in some cases.

Drupal 8 block menu settings

Alright, so what’s the problem?

While these new features is great, in the time of writing, there are still some challenges with the block system for menus.
It’s not hard to imagine usecases where you have to write different markup for the same menu, put into different regions of the layout.

The following HTML comments, are the theme hook suggestions added by the debug function, for the Main navigation blocks in the previous screenshot. This debug feature is great, and makes it easy to figure out names of the templates. You enable it by setting the debug flag to truein the twig_config part of the services.yml file in the /sites/default folder.

Header section

<!-- THEME DEBUG -->
<!-- THEME HOOK: 'menu__main' -->
<!-- FILE NAME SUGGESTIONS:
   * menu--main.html.twig
   x menu.html.twig
-->

Sidebar section

<!-- THEME DEBUG -->
<!-- THEME HOOK: 'menu__main' -->
<!-- FILE NAME SUGGESTIONS:
   * menu--main.html.twig
   x menu.html.twig
-->

As you can see from the suggestions, there is nothing to distinguish the two menus from one another.

Good old hooks to the rescue

Although Drupal 8 eliminates a lot of hooks, it does add some others to the mix; hook_theme_suggestions_alter(array &$suggestions, array $variables, $hook) being one of them. It’s pretty self explaining, and where you in Drupal 7 would use $variables[‘theme_hook_suggestions’] in a preprocess function, now you will put that same logic into this hook instead. Although the theming layer itself still leaves a lot to be wished for in Drupal 8, separation of concerns is a good thing.

Since none of the variables in that hook gives any metadata about which region the menu is inserted into, we have to do some preprocessing of the blocks that wraps the menus. This can be done through the template_preprocess_block hook. In the implementation of this hook, we will get the ID of the block and add this to the menu theme attributes. This way we can use the ID in the hook_theme_suggestions_alter hook.

This is the part of the <themename>.theme file, which by the way, is the new version of the former template.php:

/**
 * Implements hook_preprocess_block().
 */
function THEMENAME_preprocess_block(&$variables) {
  $variables['content']['#attributes']['block'] = $variables['attributes']['id'];
}
 
/**
 * Implements hook_theme_suggestions_HOOK_alter().
 */
function THEMENAME_theme_suggestions_menu_alter(array &$suggestions, array $variables) {
  // Remove the block and replace dashes with underscores in the block ID to
  // use for the hook name.
  if (isset($variables['attributes']['block'])) {
    $hook = str_replace(array('block-', '-'), array('', '_'), $variables['attributes']['block']);
    $suggestions[] = $variables['theme_hook_original'] . '__' . $hook;
  }
}

In the hook_preprocess_block() line 5 we add the ID of the block to the attributes array of the content.
Then in hook_theme_suggestions_HOOK_alter() we add a new suggestion, by suffix the original hook, with the ID of the block. The results from that is this:

Header section

<!-- THEME DEBUG -->
<!-- THEME HOOK: 'menu__main' -->
<!-- FILE NAME SUGGESTIONS:
   * menu--main.html.twig
   x menu--main--header-menu.html.twig
   * menu.html.twig
-->

Sidebar section

<!-- THEME DEBUG -->
<!-- THEME HOOK: 'menu__main' -->
<!-- FILE NAME SUGGESTIONS:
   * menu--main.html.twig
   x menu--main--siderbar-navigation.html.twig
   * menu.html.twig
-->

Pretty neat. Now it’s possible to change the markup for each menu block. Mission accomplished!

Conclusion

Drupal 8 brings a lot of new, great features into Drupal. It also does a very good job of separating the routing and the menu system from each other - which was really confusing in Drupal 7. As I already mentioned, the theming layer in Drupal 8 could be better. Fear not though, I have witnessed a core Drupal developer talk about what they will do with theming in Drupal 9. He had some great ideas, and if it get anything like what he showed in that speak, it could be very cool.
But back to Drupal 8. Although this little trick here gets the job done, it still feels a bit like a hack. Having to preprocess a block to add metadata to be used in another hook? Menus in Drupal 7 was a bitch, and although better in Drupal 8, it could seem like they still are sometimes.

Comment? Tweet me