Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I have a menu defined in WP Admin that looks like this:

alt text

I want to be able to display all the child links on the sidebar whenever I am at a parent page. For example, if the user is on my "About Us" page, I want a list of the 4 links highlighted in green to appear on the sidebar.

I looked at the documentation for wp_nav_menu() and it doesn't appear to have any built-in way to specify a particular node of a given menu to use as the starting point when generating the links.

I created a solution for a similar situation which relied on the relationships created by the page parent, but I am looking for one which uses the menu system specifically. Any help would be appreciated.

share|improve this question
1  
So you want to maintain the entire menu as a custom menu, but create a custom walker which displays it expanding only the active subtree? Like this code, but extending wp_nav_menu instead of wp_list_pages? I recently did something similar and could post the code if thats what you're looking for... –  goldenapples Oct 12 '10 at 1:51
    
@goldenapples, that's exactly what I am after. If you don't mind posting your code as an answer I would be very grateful. –  jessegavin Oct 12 '10 at 14:26
1  
I wonder that such a obvious useful functionality is not already build it. That's overall very useful for any site that does "CMS". –  hakre Feb 9 '11 at 15:54
    
I'm trying to solve the above problem or something similar. As an alternative I came up with a CSS solution here: stackoverflow.com/q/7640837/518169 –  zsero Oct 3 '11 at 23:52

11 Answers 11

up vote 37 down vote accepted

This was still on my mind so I revisited it and put together this solution, that does not rely on context that much:

add_filter( 'wp_nav_menu_objects', 'submenu_limit', 10, 2 );

function submenu_limit( $items, $args ) {

    if ( empty( $args->submenu ) ) {
        return $items;
    }

    $ids       = wp_filter_object_list( $items, array( 'title' => $args->submenu ), 'and', 'ID' );
    $parent_id = array_pop( $ids );
    $children  = submenu_get_children_ids( $parent_id, $items );

    foreach ( $items as $key => $item ) {

        if ( ! in_array( $item->ID, $children ) ) {
            unset( $items[$key] );
        }
    }

    return $items;
}

function submenu_get_children_ids( $id, $items ) {

    $ids = wp_filter_object_list( $items, array( 'menu_item_parent' => $id ), 'and', 'ID' );

    foreach ( $ids as $id ) {

        $ids = array_merge( $ids, submenu_get_children_ids( $id, $items ) );
    }

    return $ids;
}

Usage

$args = array(
    'menu'    => 'Menu Name',
    'submenu' => 'About Us',
);

wp_nav_menu( $args );
share|improve this answer
    
Lovely technique! May I ask something possibly regarding to this one: How would you display content of those listed submenu pages in template? –  daniel.tosaba Aug 22 '12 at 5:09
1  
@daniel.tosaba you will need to subclass or use filters in Walker_Nav_Menu class. Like all menu stuff too much for comment - ask new question about it? –  Rarst Aug 22 '12 at 11:18
    
1  
Such a fantastic answer. Thank you so much. This should really be a default option within WordPress. –  dotty Sep 11 '12 at 11:19
    
Hmm, I actually have a strange problem. I have a page called "Children's". And it seems that the apostrophe in the word fails to find the page. Any ideas? –  dotty Sep 11 '12 at 16:04

Hi @jessegavin:

Nav Menus are stored in a combination of custom post types and custom taxonomies. Each menu is stored as a Term (i.e. "About Menu", found in wp_terms) of a Custom Taxonomy (i.e. nav_menu, found in wp_term_taxonomy.)

Each Nav Menu Item is stored as a post of post_type=='nav_menu_item' (i.e. "About the Firm", found in wp_posts) with it's attributes stored as post meta (in wp_postmeta) using a meta_key prefix of _menu_item_* where _menu_item_menu_item_parent is the ID of your menu item's parent Nav Menu item post.

The relationship between menus and menu items is stored in wp_term_relationships where object_id relates to the $post->ID for the Nav Menu Item and the $term_relationships->term_taxonomy_id relates to the menu defined collectively in wp_term_taxonomyand wp_terms.

I'm pretty sure it would be possible to hook both 'wp_update_nav_menu' and 'wp_update_nav_menu_item' to create actual menus in wp_terms and a parallel set of relations in wp_term_taxonomy and wp_term_relationships where every Nav Menu Item that has sub-Nav Menu items also becomes it's own Nav Menu.

You'd also want to hook 'wp_get_nav_menus' (which I suggested be added to WP 3.0 based on some similar work I was doing a few months ago) to ensure that your generated Nav Menus are not displayed for manipulation by the user in the admin, otherwise they'd get out of sync really fast and then you'd have a data nightmare on your hand.

Sounds like a fun and useful project, but it is a little bit more code and testing than I can afford to tackle right now in part because anything that synchronizes data tends to be a PITA when it comes to ironing out all the bugs (and because paying clients are pressing me to get things done. :) But armed with the above info I'm pretty a motivated WordPress plugin developer could code it if they wanted to.

Of course you do realize now if you do code it you are obligated to post it back here so we can all benefit from your largesse! :-)

share|improve this answer
    
I am not sure that I am following what you're saying. I am looking for a read-only solution for displaying "sub-menus" related to the current page a user is at. Are we talking about the same thing? - I do appreciate your deeper explanation about the database schema though. –  jessegavin Oct 14 '10 at 1:24
    
@jessegavin - Yes, if you want to call wp_nav_menu() then you'll need to clone the menus because wp_nav_menu() is tightly coupled to the menu structure. The other option is to copy the wp_nav_menu() code and make the modifications required to display as a submenu. –  MikeSchinkel Oct 14 '10 at 2:34

@goldenapples: Your Walker Class does not work. But the idea is really good. I created a walker based on your idea:

class Selective_Walker extends Walker_Nav_Menu
{
    function walk( $elements, $max_depth) {

        $args = array_slice(func_get_args(), 2);
        $output = '';

        if ($max_depth < -1) //invalid parameter
            return $output;

        if (empty($elements)) //nothing to walk
            return $output;

        $id_field = $this->db_fields['id'];
        $parent_field = $this->db_fields['parent'];

        // flat display
        if ( -1 == $max_depth ) {
            $empty_array = array();
            foreach ( $elements as $e )
                $this->display_element( $e, $empty_array, 1, 0, $args, $output );
            return $output;
        }

        /*
         * need to display in hierarchical order
         * separate elements into two buckets: top level and children elements
         * children_elements is two dimensional array, eg.
         * children_elements[10][] contains all sub-elements whose parent is 10.
         */
        $top_level_elements = array();
        $children_elements  = array();
        foreach ( $elements as $e) {
            if ( 0 == $e->$parent_field )
                $top_level_elements[] = $e;
            else
                $children_elements[ $e->$parent_field ][] = $e;
        }

        /*
         * when none of the elements is top level
         * assume the first one must be root of the sub elements
         */
        if ( empty($top_level_elements) ) {

            $first = array_slice( $elements, 0, 1 );
            $root = $first[0];

            $top_level_elements = array();
            $children_elements  = array();
            foreach ( $elements as $e) {
                if ( $root->$parent_field == $e->$parent_field )
                    $top_level_elements[] = $e;
                else
                    $children_elements[ $e->$parent_field ][] = $e;
            }
        }

        $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' );  //added by continent7
        foreach ( $top_level_elements as $e ){  //changed by continent7
            // descend only on current tree
            $descend_test = array_intersect( $current_element_markers, $e->classes );
            if ( !empty( $descend_test ) ) 
                $this->display_element( $e, $children_elements, 2, 0, $args, $output );
        }

        /*
         * if we are displaying all levels, and remaining children_elements is not empty,
         * then we got orphans, which should be displayed regardless
         */
         /* removed by continent7
        if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
            $empty_array = array();
            foreach ( $children_elements as $orphans )
                foreach( $orphans as $op )
                    $this->display_element( $op, $empty_array, 1, 0, $args, $output );
         }
        */
         return $output;
    }
}

Now you can use:

<?php wp_nav_menu( 
   array(
       'theme_location'=>'test', 
       'walker'=>new Selective_Walker() ) 
   ); ?>

The output is a list containing the current root element and it's children (not their children). Def: Root element := The top level menu item that corresponds to the current page or is parent of a current page or a parent of a parent ...

This does not exactly answer the original question but almost, since there is still the top level item. This is fine for me, because I want the top level element as a headline of the sidebar. If you want to get rid of this, you might have to override display_element or use a HTML-Parser.

share|improve this answer

This is a walker extension which should do what you're looking for:

class Selective_Walker extends Walker_Nav_Menu
{

    function walk( $elements, $max_depth) {

        $args = array_slice(func_get_args(), 2);
        $output = '';

        if ($max_depth < -1) //invalid parameter
            return $output;

        if (empty($elements)) //nothing to walk
            return $output;

        $id_field = $this->db_fields['id'];
        $parent_field = $this->db_fields['parent'];

        // flat display
        if ( -1 == $max_depth ) {
            $empty_array = array();
            foreach ( $elements as $e )
                $this->display_element( $e, $empty_array, 1, 0, $args, $output );
            return $output;
        }

        /*
         * need to display in hierarchical order
         * separate elements into two buckets: top level and children elements
         * children_elements is two dimensional array, eg.
         * children_elements[10][] contains all sub-elements whose parent is 10.
         */
        $top_level_elements = array();
        $children_elements  = array();
        foreach ( $elements as $e) {
            if ( 0 == $e->$parent_field )
                $top_level_elements[] = $e;
            else
                $children_elements[ $e->$parent_field ][] = $e;
        }

        /*
         * when none of the elements is top level
         * assume the first one must be root of the sub elements
         */
        if ( empty($top_level_elements) ) {

            $first = array_slice( $elements, 0, 1 );
            $root = $first[0];

            $top_level_elements = array();
            $children_elements  = array();
            foreach ( $elements as $e) {
                if ( $root->$parent_field == $e->$parent_field )
                    $top_level_elements[] = $e;
                else
                    $children_elements[ $e->$parent_field ][] = $e;
            }
        }

        $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' );

        foreach ( $top_level_elements as $e ) {

            // descend only on current tree
            $descend_test = array_intersect( $current_element_markers, $e->classes );
            if ( empty( $descend_test ) )  unset ( $children_elements );

            $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
        }

        /*
         * if we are displaying all levels, and remaining children_elements is not empty,
         * then we got orphans, which should be displayed regardless
         */
        if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
            $empty_array = array();
            foreach ( $children_elements as $orphans )
                foreach( $orphans as $op )
                    $this->display_element( $op, $empty_array, 1, 0, $args, $output );
         }

         return $output;
    }

}

Based loosely on mfields' code I referenced in my comment earlier. All it does is check when walking the menu to see whether the current element is (1) the current menu item, or (2) an ancestor of the current menu item, and expands the subtree below it only if either of those conditions is true. Hope this works for you.

To use it, just add a "walker" argument when you call the menu, ie:

<?php wp_nav_menu( 
   array(
       'theme_location'=>'test', 
       'walker'=>new Selective_Walker() ) 
   ); ?>
share|improve this answer
    
Oh... I just re-read your question and realized that I had misunderstood it at first. This walker will show all the other top-level menu items, just not expand them. This wasn't exactly what you had wanted to do. Still, this code can be modified in any way you want. Just look at the loop through $top_level_elements and add your own test before the call to $this->display_element. –  goldenapples Oct 16 '10 at 19:46
    
Is it possible to get this class to show the depth of the current subpage? That is.. If I have a depth of three or more levels, that the third and subsequent levels are shown for the current (sub)page? At the moment, it only shows A > B, and not > C (C being the third (level) –  Zolomon Feb 20 '11 at 20:10
    
@Zolomon - I'm not sure I understand your question. This should expand the entire tree under any menu item with the classes 'current-menu-item', 'current-menu-parent', or 'current-menu-ancestor'. When I test it, it displays all the levels of subpages in the menu. What are you looking to do? –  goldenapples Feb 21 '11 at 20:39
    
Maybe you want to pass a depth parameter to the call to wp_nav_menu, in case your theme is somehow over-riding the default of 0 (show all levels)? –  goldenapples Feb 21 '11 at 20:41

I put together the following class for myself. It will find the top nav parent of the current page, or you can give it a target top nav ID in the walker constructor.

class Walker_SubNav_Menu extends Walker_Nav_Menu {
    var $target_id = false;

    function __construct($target_id = false) {
        $this->target_id = $target_id;
    }

    function walk($items, $depth) {
        $args = array_slice(func_get_args(), 2);
        $args = $args[0];
        $parent_field = $this->db_fields['parent'];
        $target_id = $this->target_id;
        $filtered_items = array();

        // if the parent is not set, set it based on the post
        if (!$target_id) {
            global $post;
            foreach ($items as $item) {
                if ($item->object_id == $post->ID) {
                    $target_id = $item->ID;
                }
            }
        }

        // if there isn't a parent, do a regular menu
        if (!$target_id) return parent::walk($items, $depth, $args);

        // get the top nav item
        $target_id = $this->top_level_id($items, $target_id);

        // only include items under the parent
        foreach ($items as $item) {
            if (!$item->$parent_field) continue;

            $item_id = $this->top_level_id($items, $item->ID);

            if ($item_id == $target_id) {
                $filtered_items[] = $item;
            }
        }

        return parent::walk($filtered_items, $depth, $args);
    }

    // gets the top level ID for an item ID
    function top_level_id($items, $item_id) {
        $parent_field = $this->db_fields['parent'];

        $parents = array();
        foreach ($items as $item) {
            if ($item->$parent_field) {
                $parents[$item->ID] = $item->$parent_field;
            }
        }

        // find the top level item
        while (array_key_exists($item_id, $parents)) {
            $item_id = $parents[$item_id];
        }

        return $item_id;
    }
}

Nav call:

wp_nav_menu(array(
    'theme_location' => 'main_menu',
    'walker' => new Walker_SubNav_Menu(22), // with ID
));
share|improve this answer

Update: I made this into a plugin. Download here.


I needed to solve this myself and eventually wound up writing a filter on the results of the menu lookup. It lets you use wp_nav_menu as normal, but choose a sub-section of the menu based on the title of the parent element. Add a submenu parameter to the menu like so:

wp_nav_menu(array(
  'menu' => 'header',
  'submenu' => 'About Us',
));

You can even go several levels deep by putting slashes in:

wp_nav_menu(array(
  'menu' => 'header',
  'submenu' => 'About Us/Board of Directors'
));

Or if you prefer with an array:

wp_nav_menu(array(
  'menu' => 'header',
  'submenu' => array('About Us', 'Board of Directors')
));

It uses a slug version of the title, which should make it forgiving of things like capitals and punctuation.

share|improve this answer
    
Is it possible to reach submenu via id? I mean page id, or post id. –  Digerkam Feb 28 '13 at 16:28

@davidn @hakre Hi, i have an ugly solution without an HTML-Parser or overriding display_element.

 class Selective_Walker extends Walker_Nav_Menu
    {
        function walk( $elements, $max_depth) {

            $args = array_slice(func_get_args(), 2);
            $output = '';

            if ($max_depth < -1) //invalid parameter
                return $output;

            if (empty($elements)) //nothing to walk
                return $output;

            $id_field = $this->db_fields['id'];
            $parent_field = $this->db_fields['parent'];

            // flat display
            if ( -1 == $max_depth ) {
                $empty_array = array();
                foreach ( $elements as $e )
                    $this->display_element( $e, $empty_array, 1, 0, $args, $output );
                return $output;
            }

            /*
             * need to display in hierarchical order
             * separate elements into two buckets: top level and children elements
             * children_elements is two dimensional array, eg.
             * children_elements[10][] contains all sub-elements whose parent is 10.
             */
            $top_level_elements = array();
            $children_elements  = array();
            foreach ( $elements as $e) {
                if ( 0 == $e->$parent_field )
                    $top_level_elements[] = $e;
                else
                    $children_elements[ $e->$parent_field ][] = $e;
            }

            /*
             * when none of the elements is top level
             * assume the first one must be root of the sub elements
             */
            if ( empty($top_level_elements) ) {

                $first = array_slice( $elements, 0, 1 );
                $root = $first[0];

                $top_level_elements = array();
                $children_elements  = array();
                foreach ( $elements as $e) {
                    if ( $root->$parent_field == $e->$parent_field )
                        $top_level_elements[] = $e;
                    else
                        $children_elements[ $e->$parent_field ][] = $e;
                }
            }

            $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' );  //added by continent7
            foreach ( $top_level_elements as $e ){  //changed by continent7
                // descend only on current tree
                $descend_test = array_intersect( $current_element_markers, $e->classes );
                if ( !empty( $descend_test ) ) 
                    $this->display_element( $e, $children_elements, 2, 0, $args, $output );
            }

            /*
             * if we are displaying all levels, and remaining children_elements is not empty,
             * then we got orphans, which should be displayed regardless
             */
             /* removed by continent7
            if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
                $empty_array = array();
                foreach ( $children_elements as $orphans )
                    foreach( $orphans as $op )
                        $this->display_element( $op, $empty_array, 1, 0, $args, $output );
             }
            */

/*added by alpguneysel  */
                $pos = strpos($output, '<a');
            $pos2 = strpos($output, 'a>');
            $topper= substr($output, 0, $pos).substr($output, $pos2+2);
            $pos3 = strpos($topper, '>');
            $lasst=substr($topper, $pos3+1);
            $submenu= substr($lasst, 0, -6);

        return $submenu;
        }
    }
share|improve this answer
    
After trying them all, Alp's solution was the only one that worked for me. However one problem with it. It on only show's the first level children, but does not show the third or fourth level children. I've been trying to for days to get it to do so. Anyone know how to modify his solution as such? PS. It won't let me add comments so need to do it as an answer. –  cchiera May 24 '11 at 1:30

The nav menu output includes lots of classes for current item, current item ancestor, etc. In some situations, I have been able to do what you want to do by letting the entire nav tree output, and then using css to pare it down to only children of the current page, etc.

share|improve this answer

I made a modified walker that should help! Not perfect - it leaves a few empty elements, but it does the trick. The modification is basically those $current_branch bits. Hope it helps someone!

class Kanec_Walker_Nav_Menu extends Walker {
/**
 * @see Walker::$tree_type
 * @since 3.0.0
 * @var string
 */
var $tree_type = array( 'post_type', 'taxonomy', 'custom' );

/**
 * @see Walker::$db_fields
 * @since 3.0.0
 * @todo Decouple this.
 * @var array
 */
var $db_fields = array( 'parent' => 'menu_item_parent', 'id' => 'db_id' );

/**
 * @see Walker::start_lvl()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param int $depth Depth of page. Used for padding.
 */
function start_lvl(&$output, $depth) {
    $indent = str_repeat("\t", $depth);
    $output .= "\n$indent<ul class=\"sub-menu\">\n";
}

/**
 * @see Walker::end_lvl()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param int $depth Depth of page. Used for padding.
 */
function end_lvl(&$output, $depth) {
    global $current_branch;
    if ($depth == 0) $current_branch = false;
    $indent = str_repeat("\t", $depth);
    $output .= "$indent</ul>\n";
}

/**
 * @see Walker::start_el()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param object $item Menu item data object.
 * @param int $depth Depth of menu item. Used for padding.
 * @param int $current_page Menu item ID.
 * @param object $args
 */
function start_el(&$output, $item, $depth, $args) {
    global $wp_query;
    global $current_branch;

    // Is this menu item in the current branch?
    if(in_array('current-menu-ancestor',$item->classes) ||
    in_array('current-menu-parent',$item->classes) ||
    in_array('current-menu-item',$item->classes)) {
        $current_branch = true; 
    }

    if($current_branch && $depth > 0) {
        $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 ) );
        $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;
        $item_output .= '</a>';
        $item_output .= $args->after;

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

}

/**
 * @see Walker::end_el()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param object $item Page data object. Not used.
 * @param int $depth Depth of page. Not Used.
 */
function end_el(&$output, $item, $depth) {
    global $current_branch;
    if($current_branch && $depth > 0) $output .= "</li>\n";
    if($depth == 0) $current_branch = 0;
}

}

share|improve this answer

Check out the code in my plugin or use it for your purpose ;)

This plugin adds enhanced "Navigation Menu" widget. It offers many options which could be set to customize the output of the custom menu through the widget.

Features include:

  • Custom hierarchy - "Only related sub-items" or "Only strictly related sub-items".
  • Starting depth and maximum level to display + flat display.
  • Display all menu items starting with the selected one.
  • Display only direct path to current element or only children of
    selected item (option to include the parent item).
  • Custom class for a widget block.
  • And almost all the parameters for the wp_nav_menu function.

http://wordpress.org/extend/plugins/advanced-menu-widget/

share|improve this answer

I find the custom walker solutions to often be overly complicated, and when using them I often run into scenarios where my menu will disappear. Like if you had a blog page and clicked into one of the blog posts.

I have written a plugin that tackles the problem from a different angle, letting you specify a 'start depth' for your menu. So if you wanted to show only the children of your current page, you would set the start depth to 1. No need to specify which section of the menu you want to show children from, it figures it out automatically.

http://mattkeys.me/products/wp-nav-plus/ for anyone who is interested in checking it out.

share|improve this answer

protected by Community May 9 '11 at 16:19

Thank you for your interest in this question. Because it has attracted low-quality answers, posting an answer now requires 10 reputation on this site.

Would you like to answer one of these unanswered questions instead?

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