0

I have a render array made by calling menu_tree_output on my main menu. The menu contains first-level options, naturally enough, and some of these have second-level children. I need to reformat my menu so that when output by render or drupal_render, each list of second-level options appears not just as <ul> and <li> elements, but as a more complicated chunk of HTML containing the links. (These chunks will implement dropdown menus with icons and subgroup headings inserted between and above links.) Reading http://themery.com/book/export/html/100 , "Using the Render API", I thought render arrays would be ideal. I could transform each list and shove the result back into the render array as raw markup. However, I'm having trouble finding the render arrays that represent the second-level menu options.

Putting my problem in more abstract terms, I've got a menu render array holding a list of lists Li. Each Li is a <ul> containing <li> and <a> elements. In the render array, I want to replace each Li by f( Li ), where f is a PHP function that I'll implement. It will insert HTML between Li's elements and around them.

I started by doing

  $tree = menu_tree_all_data( 'main-menu' );
  $options = menu_tree_output( $tree );

The second line is because, as explained in https://groups.drupal.org/node/145064 , "Geek question - Why is there no render array for links in D7?", menus are not render arrays, and need to be converted to such.

So $options is a render array. I first tested that I understood its structure well enough to loop over it and print the first- and second-level options. Here's my code:

  foreach ( $options as $option ) {
    $title = $option[ '#title' ]; 
    if ( strlen( $title ) != 0 ) {
      print $title . '<BR>';
      $sub_options = $option[ '#below' ];
      if ( count( $sub_options ) > 0 ) {
        foreach ( $sub_options as $sub_option ) {
          $sub_title = $sub_option[ '#title' ]; 
          if ( strlen( $sub_title ) != 0 ) 
            print '&nbsp;&nbsp' . $sub_title . '<BR>';
        }
      }
    }
  }

This seems to work, displaying the list:

  OPTION 1
  OPTION 2
    Sub-option 2.1
    Sub-option 2.2
  OPTION 3
    Sub-option 3.1

(The above are the names of my test options — not very interesting, I know.)

A problem I had when writing the code is that I couldn't find a spec of exactly what a menu's render array has in it. But from dpm and the source of menu_tree_output, I gathered that an option's name is in its #title element, and that its sub-options are in its #below element, which is an array.

Actually, that wasn't quite enough. Some options had empty titles which I had to skip: hence my tests on the length of $title and $sub_title. But I seemed to be going in the right direction, so I then tried another loop. This one was intended, just as a proof of concept, to add a prefix and a suffix to each list of options: both the first-level list and the second-level lists.

Here's that code:

  foreach ( $options as $option ) {
    $title = $option[ '#title' ]; 
    if ( strlen( $title ) != 0 ) { 
      $sub_options = $option[ '#below' ];
      if ( count( $sub_options ) > 0 ) {
        $sub_options[ '#prefix' ] = 'Level 2 prefix';
        $sub_options[ '#suffix' ] = 'Level 2 suffix';
      }
    }
  }
  $options[ "#prefix" ] = "Level 1 prefix";
  $options[ "#suffix" ] = "Level 1 suffix";
  print( drupal_render( $options ) );

Apparently, it's a feature of all render arrays that if you set their #prefix and #suffix elements and then render and display them, you'll see the prefix appear before the output and the suffix after it. So I thought this would be a good test. The first-level prefix and suffix do indeed appear, enclosing the menu itself.

But the second-level ones don't. Why? The source of menu_tree_output contains the line

$element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];

which certainly looks as though the #below elements should become render arrays. So why aren't #prefix and #suffix working on them?

By the way, I clear my cache before each test with drush cc all. If it matters. And my code is running in a page.tpl.php which I've written from scratch as part of a new theme. So there shouldn't be any interference from an existing theme.

  • PHP is (ostensibly) pass-by-value unless you're throwing an object into the mix. So, assuming you're trying to make changes to the original $options array before rendering it, foreach ( $options as $option ) { needs to be foreach ( $options as &$option ) {, and $sub_options = $option[ '#below' ]; needs to be $sub_options = &$option[ '#below' ]; – Clive Nov 24 '15 at 15:10
  • Thanks a lot! That worked. Now to see whether I can replace the lists by markup generated from them. – Jocelyn Ireson-Paine Nov 24 '15 at 16:08
1

As Clive pointed out, I should have assigned my sub-arrays by reference. So my code should have been:

  foreach ( $options as &$option ) {
      $title = $option[ '#title' ]; 
      if ( strlen( $title ) != 0 ) { 
        $sub_options = &$option[ '#below' ];
        if ( count( $sub_options ) > 0 ) {
          $sub_options[ '#prefix' ] = 'Level 2 prefix';
          $sub_options[ '#suffix' ] = 'Level 2 suffix';
        }
      }
    }
    $options[ "#prefix" ] = "Level 1 prefix";
    $options[ "#suffix" ] = "Level 1 suffix";
    print( drupal_render( $options ) );

That worked, printing the first- and the second-level prefix and suffix.

Armed with the knowledge that I was on the right track, I modified my code as below. Each loop cycle renders one list of sub-options and stores the resulting HTML. It then encloses it in a <table> tag, just as a demo. And it then puts the result back as raw markup. Rendering the entire menu will then display that markup, so that all my sub-options are enclosed in a table border and headed with a caption. In real life, they'd be a hidden div which reveals when you hover over it, displaying a fancy sub-menu.

One subtlety is that I had to call show, because the line $rendered_sub_options = drupal_render( $sub_options ); flags those sub-options as already printed, so that the final call to render would ignore them unless I unset the flag.

  foreach ( $options as &$option ) {
    $title = $option[ '#title' ]; 
    if ( strlen( $title ) != 0 ) { 
      $sub_options = &$option[ '#below' ];
      if ( count( $sub_options ) > 0 ) {
        $rendered_sub_options = drupal_render( $sub_options ); 
        $rendered_sub_options = entable( $rendered_sub_options ); 
        show( $sub_options );
        $sub_options[ '#markup' ] = $rendered_sub_options;
        $sub_options[ '#type' ] = 'markup';
      }
    }
  }

  function entable( $html )
  {
    return "<table border=1>" .
           "<tr><td>SUB-OPTIONS</td></tr>" .
           "<tr><td>$html</td></tr>" .
           "</table>" ;
  }

  print( drupal_render( $options ) );

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.