How do I generate a custom menu/sub-menu system using wp_get_nav_menu_items in WordPress?

I have an html structure that requires customization of the wp_nav_menu code.

This is the html I need to generate:

Read More
<ul class="main-nav">
    <li class="item">
        <a href="http://example.com/?p=123" class="title">Title</a>
        <a href="http://example.com/?p=123" class="desc">Description</a>
        <ul class="sub-menu">
            <li class="item">
                <a href="http://example.com/?p=123" class="title">Title</a>
                <a href="http://example.com/?p=123" class="desc">Description</a>
            </li>
        </ul>
    </li>
     <li class="item">
        <a href="http://example.com/?p=123" class="title">Title</a>
        <a href="http://example.com/?p=123" class="desc">Description</a>
    </li>
</ul>

I am currently using wp_get_nav_menu_items to get all the items from my menu as an array.

Right now I am able to generate the above html without the sub-menus using the following code:

<?php

$menu_name = 'main-nav';
$locations = get_nav_menu_locations()
$menu = wp_get_nav_menu_object( $locations[ $menu_name ] );
$menuitems = wp_get_nav_menu_items( $menu->term_id, array( 'order' => 'DESC' ) );

foreach ( $menuitems as $item ):

    $id = get_post_meta( $item->ID, '_menu_item_object_id', true );
    $page = get_page( $id );
    $link = get_page_link( $id ); ?>

    <li class="item">
        <a href="<?php echo $link; ?>" class="title">
            <?php echo $page->post_title; ?>
        </a>
        <a href="<?php echo $link; ?>" class="desc">
            <?php echo $page->post_excerpt; ?>
        </a>
    </li>

<?php endforeach; ?>

I would have generated the menu using the wp_nav_menu function but I still need the description shown using $page->post_excerpt.

I’ve found that there is a property for each item called $item->menu_item_parent which gives the ID of the parent menu item.

How would I generate the sub-menu in my foreach loop?
Or is there a really simple way using wp_nav_menu which Google forgot to mention?

Related posts

Leave a Reply

6 comments

  1. For anyone who tackles something similar here’s my solution:

    Quick code example on a gist

    Here’s the code on a github gist for anyone who wants to get in on the copy paste action.

    TL;DR

    TL;DR Loop over list, drill down if there’s a sub menu, close if we reach the end of the sub menu and menu.

    Complete Code explanation

    Firstly get the menu items as a flat array:

    <?php
    $menu_name = 'main_nav';
    $locations = get_nav_menu_locations();
    $menu = wp_get_nav_menu_object( $locations[ $menu_name ] );
    $menuitems = wp_get_nav_menu_items( $menu->term_id, array( 'order' => 'DESC' ) );
    ?>
    

    Then iterate over the array of the menu items:

    <nav>
    <ul class="main-nav">
        <?php
        $count = 0;
        $submenu = false;
    
        foreach( $menuitems as $item ):
            // set up title and url
            $title = $item->title;
            $link = $item->url;
    
            // item does not have a parent so menu_item_parent equals 0 (false)
            if ( !$item->menu_item_parent ):
    
            // save this id for later comparison with sub-menu items
            $parent_id = $item->ID;
        ?>
    

    Write the first parent item <li>:

        <li class="item">
            <a href="<?php echo $link; ?>" class="title">
                <?php echo $title; ?>
            </a>
        <?php endif; ?>
    

    Check that this items’ parent id matches the stored parent id:

            <?php if ( $parent_id == $item->menu_item_parent ): ?>
    

    Start sub-menu <ul> and set $submenu flag to true for later referance:

                <?php if ( !$submenu ): $submenu = true; ?>
                <ul class="sub-menu">
                <?php endif; ?>
    

    Write the sub-menu item:

                    <li class="item">
                        <a href="<?php echo $link; ?>" class="title"><?php echo $title; ?></a>
                    </li>
    

    If the next item does not have the same parent id and we have a sub-menu declared then close the sub-menu <ul>

                <?php if ( $menuitems[ $count + 1 ]->menu_item_parent != $parent_id && $submenu ): ?>
                </ul>
                <?php $submenu = false; endif; ?>
    
            <?php endif; ?>
    

    Again, if the next item in the array does not have the same parent id close the <li>

        <?php if ( $menuitems[ $count + 1 ]->menu_item_parent != $parent_id ): ?>
        </li>                           
        <?php $submenu = false; endif; ?>
    
    <?php $count++; endforeach; ?>
    
      </ul>
    </nav>
    
  2. Your best bet is to make your own Walker class to tailor the output to your needs. Something like this:

    class Excerpt_Walker extends Walker_Nav_Menu
    {
        function start_el(&$output, $item, $depth, $args)
        {
            global $wp_query;
            $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
    
            $class_names = $value = '';
    
            $classes = empty( $item->classes ) ? array() : (array) $item->classes;
            $classes[] = 'menu-item-' . $item->ID;
    
            $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
            $class_names = ' class="' . esc_attr( $class_names ) . '"';
    
            $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
            $id = strlen( $id ) ? ' id="' . esc_attr( $id ) . '"' : '';
    
            $output .= $indent . '<li' . $id . $value . $class_names .'>';
    
            $attributes  = ! empty( $item->attr_title ) ? ' title="'  . esc_attr( $item->attr_title ) .'"' : '';
            $attributes .= ! empty( $item->target )     ? ' target="' . esc_attr( $item->target     ) .'"' : '';
            $attributes .= ! empty( $item->xfn )        ? ' rel="'    . esc_attr( $item->xfn        ) .'"' : '';
            $attributes .= ! empty( $item->url )        ? ' href="'   . esc_attr( $item->url        ) .'"' : '';
    
            $item_output = $args->before;
            $item_output .= '<a'. $attributes .'>';
    
            $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
    
            /*GET THE EXCERPT*/
            $q = new WP_Query(array('post__in'=>$item->object_id));
            if($q->have_posts()) : while($q->have_posts()) : $q->the_post();
                $item_output .= '<span class="menu-excerpt">'.get_the_excerpt().'</span>';
            endwhile;endif;
            /*****************/
    
            $item_output .= '</a>';
            $item_output .= $args->after;
    
            $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
        }
    }
    

    And then call it like so:

    <?php
    wp_nav_menu(array('walker' => new Excerpt_Walker()));
    ?>
    

    Everything before and after the GET THE EXCERPT marker in my example was a direct copy of the start_el function in wp-includes/nav-menu-template.php. My example uses WP_Query to determine if the post/page has an excerpt, and places the excerpt in between span tags after the link title.

    An idea would be to make the span tags appear only upon hover, which can be done using CSS.

    More information on Walkers here:

    WP Nav Menu Codex

    Using The Walker Class

    Another Decent Example Using the Walker Class

  3.       //Just set your variable and apply.. for sub menu
         <ul>
         <?php
    
         $activeclass = '';
        $activeclass1='';
         $count=0;
        $submenu = FALSE;
        foreach ( $primaryNav as $navItem ) {
                $activeclass = '';  
             if($navItem->object_id == $getid){
                $activeclass = 'class="live-act"';
             }
             if (!$navItem->menu_item_parent ){
                $parent_id = $navItem->ID;
            echo '<li><a href="'.$navItem->url.'" '.$activeclass.'   title="'.$navItem->title.'">'.$navItem->title.'</a>';
             }
             ?>
            <?php if ( $parent_id == $navItem->menu_item_parent ) { ?>
          <?php if ( !$submenu ): $submenu = true; ?>
            <ul>
            <?php endif; ?>
                 <li>
                    <a href="<?php echo $navItem->url; ?>" class="title"><?php echo $navItem->title; ?></a>
                </li>
           <?php if ( $primaryNav[ $count + 1 ]->menu_item_parent != $parent_id && $submenu ){ ?>
            </ul>
            <?php $submenu = false; 
           }
            ?>
            <?php }
             if ( $primaryNav[ $count + 1 ]->menu_item_parent != $parent_id ){ ?>
            </li>                            
                <?php $submenu = false; } ?>
    
            <?php $count++;   ?>
    
    
            <?php } ?>
             </ul>
    
  4. Complete Code explanation…..

    First Create two below two functions in functions.php file

     function get_menu_id( $location ){
                    //get all the location....
                    $locations =get_nav_menu_locations();
        
                    //get object id by location.....
                    $menu_id = $locations[$location];
                    
                    return !empty($menu_id) ? $menu_id : '';
                }
    
    
                function get_child_menu_items($menu_array , $parent_id){
                    $child_menu = [];
                    if(!empty($menu_array) && is_array($menu_array)){
                        foreach($menu_array as $menu){
                            if( intval($menu->menu_item_parent) === $parent_id){
                                array_push($child_menu , $menu);
                            }
                        }
                    }
                    return $child_menu;
                }
    

    then

    $menu_name = 'menu_name';
                    $location = get_nav_menu_locations();
                    $menu = wp_get_nav_menu_object($location[$menu_name]);
                    $menuitems = wp_get_nav_menu_items( $menu->term_id);
                    $header_menu_id = get_menu_id('primary-menu');
                    
                    $header_menus = wp_get_nav_menu_items($header_menu_id);
    

    now this is my html and php code….

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
    
    
                        <?php
                            if(!empty($header_menus)  && is_array($header_menus)){
                                ?>
                                <ul class="navbar-nav ml-auto">
                                    <?php
                                        foreach($header_menus as $menu_item){
                                            if(!$menu_item->menu_item_parent){ //checking the item are parent?
                                                $child_menu_items = get_child_menu_items($header_menus, $menu_item->ID);
                                                $has_children = !empty($child_menu_items) && is_array($child_menu_items);
    
                                                if(! $has_children){
                                                    ?>
                                                    <li class="nav-item">
                                                        <a class="nav-link" href="<?php echo esc_url($menu_item -> url); ?>"> <?php echo esc_html($menu_item->title); ?></a> 
                                                    </li>
                                                    <?php
                                                }else{
                                                    ?>
                                                <li class="nav-item dropdown">
                                                    <a class="nav-link dropdown-toggle" href="<?php echo esc_url($menu_item -> url); ?>" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                                    <?php echo esc_html($menu_item->title); ?>
                                                    </a>
                                                    <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                                                        <?php
                                                            foreach($child_menu_items as $child){
                                                                ?>
                                                                <a class="dropdown-item" href="<?php echo esc_url($child->url); ?>" > <?php echo esc_html($child->title); ?></a> 
                                                                <?php
                                                            }
                                                        ?>        
                                                    </div>
                                                </li>
                                                    <?php
                                                }
                                            }
                                        }
                                    ?>
                                </ul>
                                <?php
                            }
                        ?>
    
  5. Pleae try this. It works for me.

    <?php
            $menuLocations = get_nav_menu_locations();
            $menuID = $menuLocations['primary'];
            $menu = wp_get_nav_menu_object($menuLocations['primary']);
            $menuitems = wp_get_nav_menu_items($menu->term_id, array('order' => 'DESC'));
            $ParentArray = array();
            $count = 0;
            $submenu = false;
    
        foreach ($menuitems as $menu_item) {
            $link = $menu_item->url;
            $title = $menu_item->title;
            if ($menu_item->menu_item_parent == 0   ) {
                $parent_id = $menu_item->ID;
                echo  '<li class="main-li dropmenu"><a href="' .$menu_item->url . '">'.$menu_item->title .'</a>';
            }
            if ($parent_id == $menu_item->menu_item_parent ) {
                if (!$submenu ) {
                    $submenu = true;
                    echo '<ul class="level1">';
                }
                 echo '<li><a href="'.$link.'">'.$title.'</a></li>';
                if ($menuitems[ $count + 1 ]->menu_item_parent != $parent_id && $submenu ) {
                    echo '</ul>';
                    $submenu = false;
                }
            }
            if ($menuitems[ $count + 1 ]->menu_item_parent != $parent_id ) {
                echo '</li>';
                $submenu = false;
            }
            $count++;
        }
        ?>
    
  6. If your theme uses has_nav_menu('<menu-name>') in the header or footer.php(for example), you can go to Appearance > Menus and select which menu or sub-menu to configure.