2

Here is a simple output of the default navigation menu mark-up:

<ul>
    <li><a>First Item</a>
        <ul class="sub-menu">
            <li><a>Some Child Item</a></li>
            <li><a>Some Child Item</a></li>
        </ul>
    </li>
    <li><a>Second Item</a></li>
    <li><a>Third Item</a></li>
</ul>

I'm trying to add the parent item's title to the sub-menu section. So, I'm trying to achieve this:

<ul>
    <li><a>First Item</a>
        <ul class="sub-menu">
            <h3>First Item</h3> <!-- I'm looking for this -->
            <li><a>Some Child Item</a></li>
            <li><a>Some Child Item</a></li>
        </ul>
    </li>
    <li><a>Second Item</a></li>
    <li><a>Third Item</a></li>
</ul>

I've been digging into the Walker_Nav_Menu class, and its methods. The method that is responsible for outputting the <ul> is the start_lvl method, and I can't find a way to grab the parent element's object or ID to retrieve its title. Here's the content of this function:

public function start_lvl( &$output, $depth = 0, $args = array() ) {
        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
            $t = '';
            $n = '';
        } else {
            $t = "\t";
            $n = "\n";
        }
        $indent = str_repeat( $t, $depth );

        // Default class.
        $classes = array( 'sub-menu' );

        /**
         * Filters the CSS class(es) applied to a menu list element.
         *
         * @since 4.8.0
         *
         * @param array    $classes The CSS classes that are applied to the menu `<ul>` element.
         * @param stdClass $args    An object of `wp_nav_menu()` arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

        $output .= "{$n}{$indent}<ul$class_names>{$n}";
    }

Is it possible to do this?

  • Use a custom walker that extends Walker_Nav_Menu and overrides the important parts. – janh Dec 26 '17 at 17:17
  • @janh The problem is, there is no variable containing the parent ID passed to the start_lvl() function in the original class. – Jack Johansson Dec 26 '17 at 17:19
  • If you extend the class, you can add that, though. You're not limited to changing one method, just copy display_element from the Walker class (class-wp-walker.php) and add the current element to $cb_args where start_lvl() is called, like it's done with start_el() a few lines above that. – janh Dec 26 '17 at 17:49
4

I believe wp_nav_menu() handles the top-level list wrapping so start_lvl method would only handle submenu items ( from my understanding ). It also doesn't give us access to the $item which we need to get the parent.

Below I've setup a boolean variable to track whether we should be showing our title or not. It also keeps it HTML valid, lists can only contain list items so it may be better to style list items with a specific class. The below code is 90% taken from the Core WP_Nav_Menu Walker

First we need to setup our variable:

class Title_Sub_Menus extends Walker_Nav_Menu {

    /**
     * Track Whether to show parent title
     *
     * @var Boolean
     */
    private $show_parent_title = false;

    /* ... */

}

Next, we need to turn on our switch variable whenever a new submenu is created:

class Title_Sub_Menus extends Walker_Nav_Menu {

    /* ... */

    /**
     * Starts the list before the elements are added.
     *
     * @since 3.0.0
     *
     * @see Walker::start_lvl()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
                $t = '';
                $n = '';
        } else {
                $t = "\t";
                $n = "\n";
        }

        $indent = str_repeat( $t, $depth );

        // Default class.
        $classes = array( 'sub-menu' );

        // Enable Submenu Title
        $this->show_parent_title = true;

        /**
         * Filters the CSS class(es) applied to a menu list element.
         *
         * @since 4.8.0
         *
         * @param array    $classes The CSS classes that are applied to the menu `<ul>` element.
         * @param stdClass $args    An object of `wp_nav_menu()` arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        $output .= "{$n}{$indent}<ul$class_names>{$n}";

    }

    /* ... */

} // END class Title_Sub_Menus

This is where things get a little tricky. As far as I know, Menu Item Object titles are not saved with the item if it hasn't changed from the original title. If the original page title is "WordPress Page" and you change the menu item title to "WordPress" after adding it to the menu, then it's saved to the Menu Item. We also should check against terms. Kudos to Alex Sancho's answer.

Class Title_Sub_Menus extends Walker_Nav_Menu {

    /* ... */

    /**
     * Starts the element output.
     *
     * @since 3.0.0
     * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
     *
     * @see Walker::start_el()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param WP_Post  $item   Menu item data object.
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     * @param int      $id     Current item ID.
     */
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {

        // Maybe show parent title
        if( 0 === $depth && $this->show_parent_title ) {

            // Grab the title if the user HAS changed it from the original
            $title = get_the_title( $item->menu_item_parent );

            if( empty( $title ) ) {

                // Title is the original title, grab it from the original post or term.
                $object_id  = get_post_meta( $item->menu_item_parent, '_menu_item_object_id', true );
                $object     = get_post_meta( $item->menu_item_parent, '_menu_item_object',    true );
                $type       = get_post_meta( $item->menu_item_parent, '_menu_item_type',      true );

                if ( 'post_type' === $type ) {
                    $title = get_post( $object_id )->post_title;
                } elseif ( 'taxonomy' === $type) {
                    $title = get_term( $object_id, $object )->name;
                }

            }

            $output .= sprintf( '<li class="parentTitle"><h3>%1$s</h3></li>', $title ); // Display our title

            // Parent title displayed, turn off switch
            $this->show_parent_title = false;

        }

        /* ... */

    }

} // END class Title_Sub_Menus

Full Class

class Title_Sub_Menus extends Walker_Nav_Menu {

    /**
     * Track Whether to show parent title
     *
     * @var Boolean
     */
    private $show_parent_title = false;


    /**
     * Starts the list before the elements are added.
     *
     * @since 3.0.0
     *
     * @see Walker::start_lvl()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
                $t = '';
                $n = '';
        } else {
                $t = "\t";
                $n = "\n";
        }

        $indent = str_repeat( $t, $depth );

        // Default class.
        $classes = array( 'sub-menu' );

        $this->show_parent_title = true;

        /**
         * Filters the CSS class(es) applied to a menu list element.
         *
         * @since 4.8.0
         *
         * @param array    $classes The CSS classes that are applied to the menu `<ul>` element.
         * @param stdClass $args    An object of `wp_nav_menu()` arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        $output .= "{$n}{$indent}<ul$class_names>{$n}";

    }


    /**
     * Starts the element output.
     *
     * @since 3.0.0
     * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
     *
     * @see Walker::start_el()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param WP_Post  $item   Menu item data object.
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     * @param int      $id     Current item ID.
     */
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {

        // Maybe show parent title
        if( 0 === $depth && $this->show_parent_title ) {

            $object_id  = get_post_meta( $item->menu_item_parent, '_menu_item_object_id', true );
            $object     = get_post_meta( $item->menu_item_parent, '_menu_item_object',    true );
            $type       = get_post_meta( $item->menu_item_parent, '_menu_item_type',      true );
            $title      = get_the_title( $item->menu_item_parent );

            if( empty( $title ) ) {

                if ( 'post_type' == $type ) {
                    $title = get_post( $object_id )->post_title;
                } elseif ( 'taxonomy' == $type) {
                    $title = get_term( $object_id, $object )->name;
                }

            }

            $output .= sprintf( '<li class="parentTitle"><h3>%1$s</h3></li>', $title ); // OR post_parent if dynamically generated
            $this->show_parent_title = false;

        }

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
                $t = '';
                $n = '';
        } else {
                $t = "\t";
                $n = "\n";
        }

        $indent = ( $depth ) ? str_repeat( $t, $depth ) : '';
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;

        /**
         * Filters the arguments for a single nav menu item.
         *
         * @since 4.4.0
         *
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param WP_Post  $item  Menu item data object.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
        /**
         * Filters the CSS class(es) applied to a menu item's list item element.
         *
         * @since 3.0.0
         * @since 4.1.0 The `$depth` parameter was added.
         *
         * @param array    $classes The CSS classes that are applied to the menu item's `<li>` element.
         * @param WP_Post  $item    The current menu item.
         * @param stdClass $args    An object of wp_nav_menu() arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        /**
         * Filters the ID applied to a menu item's list item element.
         *
         * @since 3.0.1
         * @since 4.1.0 The `$depth` parameter was added.
         *
         * @param string   $menu_id The ID that is applied to the menu item's `<li>` element.
         * @param WP_Post  $item    The current menu item.
         * @param stdClass $args    An object of wp_nav_menu() arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
        $output .= $indent . '<li' . $id . $class_names .'>';
        $atts = array();
        $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
        $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
        $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
        $atts['href']   = ! empty( $item->url )        ? $item->url        : '';
        /**
         * Filters the HTML attributes applied to a menu item's anchor element.
         *
         * @since 3.6.0
         * @since 4.1.0 The `$depth` parameter was added.
         *
         * @param array $atts {
         *     The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
         *
         *     @type string $title  Title attribute.
         *     @type string $target Target attribute.
         *     @type string $rel    The rel attribute.
         *     @type string $href   The href attribute.
         * }
         * @param WP_Post  $item  The current menu item.
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
        $attributes = '';
        foreach ( $atts as $attr => $value ) {
                if ( ! empty( $value ) ) {
                        $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
                        $attributes .= ' ' . $attr . '="' . $value . '"';
                }
        }
        /** This filter is documented in wp-includes/post-template.php */
        $title = apply_filters( 'the_title', $item->title, $item->ID );
        /**
         * Filters a menu item's title.
         *
         * @since 4.4.0
         *
         * @param string   $title The menu item's title.
         * @param WP_Post  $item  The current menu item.
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before . $title . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;
        /**
         * Filters a menu item's starting output.
         *
         * The menu item's starting output only includes `$args->before`, the opening `<a>`,
         * the menu item's title, the closing `</a>`, and `$args->after`. Currently, there is
         * no filter for modifying the opening and closing `<li>` for a menu item.
         *
         * @since 3.0.0
         *
         * @param string   $item_output The menu item's starting HTML output.
         * @param WP_Post  $item        Menu item data object.
         * @param int      $depth       Depth of menu item. Used for padding.
         * @param stdClass $args        An object of wp_nav_menu() arguments.
         */
        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

} // END class Title_Sub_Menus

It seems a bit messy / strange to me there's not an easier or more straight-forward way to do this so this is one possible solution. Hopefully someone comes along with a more intuitive solution and explanation.

  • 1
    Thanks for the solution Alex. There was however a small thing to consider. We should check if the depth is 0 and then set the boolean to true, otherwise it will print the title for child <ul> elements too. – Jack Johansson Dec 28 '17 at 0:15

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.