11

I'm trying to create a menu in Drupal 8 that is just sibling links of the current page. So if the menu is:

  • Home
  • Parent 1
    • Sub-parent 1
      • Child 1
    • Sub-parent 2
      • Child 2
      • Child 3
      • Child 4
  • Parent 2

When I'm on the "Child 3" page, I want a menu block to link to look like this:

  • Child 2
  • Child 3
  • Child 4

I know how to do this in D7, I think, but I'm having a hard time translating that knowledge to D8. Is this even something that is doable in D8? And if it is, can someone point me in the right direction on how to do it?

Thanks!

Clarification: The child level needs to be variable, so that menu items with different depths can display their siblings. So for instance, in addition to wanting a menu for the children, I'd need a menu for the sub-parents and a menu for the parents and so forth. I also have no control over/knowledge of how deep the menu goes and if it goes that deep on all sections.

4 Answers 4

19

So, I ended up figuring out some code that would let me do this, by creating a custom block and, in the build method, outputting the menu with transformers added to it. This is the link I used to figure out how to get the menu in the block and add transformers to it: http://alexrayu.com/blog/drupal-8-display-submenu-block. My build() ended up looking like this:

$menu_tree = \Drupal::menuTree();
$menu_name = 'main';

// Build the typical default set of menu tree parameters.
$parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);

// Load the tree based on this set of parameters.
$tree = $menu_tree->load($menu_name, $parameters);

// Transform the tree using the manipulators you want.
$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'),
  // Remove all links outside of siblings and active trail
  array('callable' => 'intranet.menu_transformers:removeInactiveTrail'),
);
$tree = $menu_tree->transform($tree, $manipulators);

// Finally, build a renderable array from the transformed tree.
$menu = $menu_tree->build($tree);

return array(
  '#markup' => \Drupal::service('renderer')->render($menu),
  '#cache' => array(
    'contexts' => array('url.path'),
  ),
);

The transformer is a service, so I added an intranet.services.yml to my intranet module, pointing to the class that I ended up defining. The class had three methods: removeInactiveTrail(), which called getCurrentParent() to get the parent of the page the user was currently on, and stripChildren(), which stripped the menu down to only the children of the current menu item and its siblings (ie: removed all submenus that weren't in the active trail).

This is what that looked like:

/**
 * Removes all link trails that are not siblings to the active trail.
 *
 * For a menu such as:
 * Parent 1
 *  - Child 1
 *  -- Child 2
 *  -- Child 3
 *  -- Child 4
 *  - Child 5
 * Parent 2
 *  - Child 6
 * with current page being Child 3, Parent 2, Child 6, and Child 5 would be
 * removed.
 *
 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
 *   The menu link tree to manipulate.
 *
 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
 *   The manipulated menu link tree.
 */
public function removeInactiveTrail(array $tree) {
  // Get the current item's parent ID
  $current_item_parent = IntranetMenuTransformers::getCurrentParent($tree);

  // Tree becomes the current item parent's children if the current item
  // parent is not empty. Otherwise, it's already the "parent's" children
  // since they are all top level links.
  if (!empty($current_item_parent)) {
    $tree = $current_item_parent->subtree;
  }

  // Strip children from everything but the current item, and strip children
  // from the current item's children.
  $tree = IntranetMenuTransformers::stripChildren($tree);

  // Return the tree.
  return $tree;
}

/**
 * Get the parent of the current active menu link, or return NULL if the
 * current active menu link is a top-level link.
 *
 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
 *   The tree to pull the parent link out of.
 * @param \Drupal\Core\Menu\MenuLinkTreeElement|null $prev_parent
 *   The previous parent's parent, or NULL if no previous parent exists.
 * @param \Drupal\Core\Menu\MenuLinkTreeElement|null $parent
 *   The parent of the current active link, or NULL if not parent exists.
 *
 * @return \Drupal\Core\Menu\MenuLinkTreeElement|null
 *   The parent of the current active menu link, or NULL if no parent exists.
 */
private function getCurrentParent($tree, $prev_parent = NULL, $parent = NULL) {
  // Get active item
  foreach ($tree as $leaf) {
    if ($leaf->inActiveTrail) {
      $active_item = $leaf;
      break;
    }
  }

  // If the active item is set and has children
  if (!empty($active_item) && !empty($active_item->subtree)) {
    // run getCurrentParent with the parent ID as the $active_item ID.
    return IntranetMenuTransformers::getCurrentParent($active_item->subtree, $parent, $active_item);
  }

  // If the active item is not set, we know there was no active item on this
  // level therefore the active item parent is the previous level's parent
  if (empty($active_item)) {
    return $prev_parent;
  }

  // Otherwise, the current active item has no children to check, so it is
  // the bottommost and its parent is the correct parent.
  return $parent;
}


/**
 * Remove the children from all MenuLinkTreeElements that aren't active. If
 * it is active, remove its children's children.
 *
 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
 *   The menu links to strip children from non-active leafs.
 *
 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
 *   A menu tree with no children of non-active leafs.
 */
private function stripChildren($tree) {
  // For each item in the tree, if the item isn't active, strip its children
  // and return the tree.
  foreach ($tree as &$leaf) {
    // Check if active and if has children
    if ($leaf->inActiveTrail && !empty($leaf->subtree)) {
      // Then recurse on the children.
      $leaf->subtree = IntranetMenuTransformers::stripChildren($leaf->subtree);
    }
    // Otherwise, if not the active menu
    elseif (!$leaf->inActiveTrail) {
      // Otherwise, it's not active, so we don't want to display any children
      // so strip them.
      $leaf->subtree = array();
    }
  }

  return $tree;
}

Is this the best way to do it? Probably not. But it at least provides a starting place for people who need to do something similar.

3
  • This is pretty much what footermap does. +1 for using menu.tree service.
    – mradcliffe
    Commented Mar 31, 2016 at 15:16
  • 3
    Could you please tell what code should be placed in service.yml file? How to point a class from service.yml file?
    – siddiq
    Commented Dec 13, 2017 at 3:59
  • How to exclude the parent/s menu link?
    – Permana
    Commented Nov 12, 2018 at 6:04
7

Siblings Menu Block

With the help of @Icubes answer and MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters we can simply get the current route's active menu trail. Having that we also have the parent menu item. Setting that as starting point via MenuTreeParameters::setRoot to build a new tree gives you the desired siblings menu.

$menu_name = 'main';
$menu_tree = \Drupal::menuTree();

// This one will give us the active trail in *reverse order*.
// Our current active link always will be the first array element.
$parameters   = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
$active_trail = array_keys($parameters->activeTrail);

// But actually we need its parent.
// Except for <front>. Which has no parent.
$parent_link_id = isset($active_trail[1]) ? $active_trail[1] : $active_trail[0];

// Having the parent now we set it as starting point to build our custom
// tree.
$parameters->setRoot($parent_link_id);
$parameters->setMaxDepth(1);
$parameters->excludeRoot();
$tree = $menu_tree->load($menu_name, $parameters);

// Optional: Native sort and access checks.
$manipulators = [
  ['callable' => 'menu.default_tree_manipulators:checkNodeAccess'],
  ['callable' => 'menu.default_tree_manipulators:checkAccess'],
  ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
$tree = $menu_tree->transform($tree, $manipulators);

// Finally, build a renderable array and enable proper caching.
$menu = $menu_tree->build($tree);

$build = [
  '#markup' => \Drupal::service('renderer')->render($menu),
  '#cache' => [
    'contexts' => ['url'], // For .active classes.
    'tags' => ['config:system.menu.' . $menu_name], // For menu changes.
  ],
];

return $build;
0
4

Setting the root on the current link might do the tricks :

$menu_tree = \Drupal::menuTree();
$menu_name = 'main';

$parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
$currentLinkId = reset($parameters->activeTrail);
$parameters->setRoot($currentLinkId);
$tree = $menu_tree->load($menu_name, $parameters);

// Transform the tree using the manipulators you want.
$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'),
);
$tree = $menu_tree->transform($tree, $manipulators);
2
  • Nah, unfortunately this only shows children. But not siblings. OP wants siblings.
    – leymannx
    Commented Apr 9, 2018 at 14:18
  • This helped me to show the children of the current active menu thanks!
    – Beebee
    Commented Mar 2, 2021 at 16:59
3

Drupal 8 has Menu Block functionality built in the core only thing you have do is to create a new menu block in the Block Ui and configure that.

That happens by:

  • Placing a new block and then selecting the menu you want to create a block for.
  • In the block configuration you have to select the "Initial menu level" to be 3.
  • You might also want to set the "Maximum number of menu levels to display" to 1 in case you only want to print menu items from the third level.
3
  • Unfortunately, I can't be sure which level the page is going to be at, so I can't just create a menu block for it. There's also the possibility that it may need to be variable levels, depending on what the site structure ends up being. Commented Dec 8, 2015 at 14:42
  • menu_block for Drupal 8 doesn't currently include functionality for following the current node, patches in review here; drupal.org/node/2756675
    – Christian
    Commented Oct 24, 2016 at 3:40
  • 1
    OK for static use. But not for dynamic use as in "Place a block on every page and show the current page's siblings no matter what level you are currently in."
    – leymannx
    Commented Apr 9, 2018 at 12:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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