WordPress – how do I know if a menu item has children?

I’m developing a wordpress theme with nested submenus. I need to make the elements with no children visually different from the ones that have children. Right now I have this menu, but that could change:

A
  a1
  a2
B
  b1
  b2
C

As you can see, A and B have children. C doesn’t – I need it to be different in the CSS level.

Read More

Ideally, I would like to have a has-children class in A and B, but not in C.

So far I’ve managed to create a “Menu Walker” PHP class that I can instantiate and pass to wp_nav_menu . Its constructor looks like this:

class My_Walker_Nav_Menu extends Walker_Nav_Menu {
  function start_el(&$output, $item, $depth, $args) {
    ...
    if(??? $item has children???) {
      // I know what to do here
    }
  }
}

So, how do I tell whether $item has children, or is a leaf?

EDIT: this question was answered by someone called “keesiemeijer” in the WordPress forums. I’m leaving this bounty expired just in case he wants to reclaim it. Otherwise, I’ll be marking my own answer as valid.

Related posts

Leave a Reply

14 comments

  1. Add this to functions.php it will add the ‘dropdown’ class to parents

    New way beter for performance

    function menu_set_dropdown( $sorted_menu_items, $args ) {
        $last_top = 0;
        foreach ( $sorted_menu_items as $key => $obj ) {
            // it is a top lv item?
            if ( 0 == $obj->menu_item_parent ) {
                // set the key of the parent
                $last_top = $key;
            } else {
                $sorted_menu_items[$last_top]->classes['dropdown'] = 'dropdown';
            }
        }
        return $sorted_menu_items;
    }
    add_filter( 'wp_nav_menu_objects', 'menu_set_dropdown', 10, 2 );
    

    Old: intensive on the DB

    add_filter( 'nav_menu_css_class', 'check_for_submenu', 10, 2);
    function check_for_submenu($classes, $item) {
        global $wpdb;
        $has_children = $wpdb->get_var("SELECT COUNT(meta_id) FROM wp_postmeta WHERE meta_key='_menu_item_menu_item_parent' AND meta_value='".$item->ID."'");
        if ($has_children > 0) array_push($classes,'dropdown'); // add the class dropdown to the current list
        return $classes;
    }
    
  2. Simple you use this way:

    Explain:
    I create menu with “walker”:

    $walker = new Nav_Walker;
    wp_nav_menu(array(
            'container'=>'nav',
            'container_class'=>'menu',
            'menu_class'=>'list-unstyled list-inline',
            'walker'=>$walker
        ));
    

    Class Walker:

    class Nav_Walker extends Walker_Nav_Menu
    { 
          public function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0)
        {
            if($args->walker->has_children)
            {
                //code...
            }   
        }
    }
    

    We have object ‘walker’, you can var_dump($args) to see more things.
    I’m using for my project !

  3. It seems that the problem has finally been addressed. The latest WordPress beta as of current writing 4.0, has updated the Walker_Nav_Menu class and added a $has_children property.

    /**
     * Whether the current element has children or not.
     *
     * To be used in start_el().
     *
     * @since 4.0.0
     * @access protected
     * @var bool
     */
    protected $has_children;
    

    So, we don’t need to hack function display_element(...) anymore.

  4. I asked in the WordPress forum and keesiemeijer pointed me to this other post, in which they did the following:

    Instead of modifying start_el, they modified display_element, adding the following two lines (lines 37-38 here):

    //display this element (THESE ARE NOT THE LINES)
    if ( is_array( $args[0] ) )
      $args[0]['has_children'] = ! empty( $children_elements[$element->$id_field] );
    
    // THESE TWO ARE THE LINES:               
    if( ! empty( $children_elements[$element->$id_field] ) )
      array_push($element->classes,'parent');
    

    I’ve left the previous two lines as a spacial reference, and also as a comment to other answers in this post. It seems that wordpress is “trying” to set a ´has_children´ property in $args, but it’s either doing it wrong or in a way I don’t understand. In any case, that has_children parameter is never passed down to start_el (see sample var_dump of an $args here)

    This might be a bug on the WordPress version I’ve got (3.2.1) and might have been fixed in the most recent version.

    In any case, the answer I got in the WordPress forum is the one that helped me fix it, so I consider this settled. I’ll wait for the bounty to expire just in case keesiemeijer wants to put his answer here.

  5. class My_Walker_Nav_Menu extends Walker_Nav_Menu {
      function start_el(&$output, $item, $depth, $args) {
        ...
        if($args['has_children']) {
          // I know what to do here
        }
      }
    }
    
  6. Add this to your functions.php

    add_filter('wp_nav_menu_objects', 'menu_has_children', 10, 2);
    
    function menu_has_children($sorted_menu_items, $args) {
        $parents = array();
        foreach ( $sorted_menu_items as $key => $obj )
                $parents[] = $obj->menu_item_parent;
        foreach ($sorted_menu_items as $key => $obj)
            $sorted_menu_items[$key]->has_children = (in_array($obj->ID, $parents)) ? true : false;
        return $sorted_menu_items;
    }
    

    Then in your walker your can check if $item->has_children is true or false

  7. Kikito’s answer above gets the trick done, but not in the most reusable way. In my view, the better approach is like this:

    function display_element($element, &$children_elements, $max_depth, $depth=0, $args, &$output) {
        // the first few lines of the method...
    
        //display this element; handle either arrays or objects gracefully
        if ( is_array( $args[0] ) )
            $args[0]['has_children'] = ! empty( $children_elements[$element->$id_field] );
    
        elseif ( is_object($args[0]) )
            $args[0]->has_children =  ! empty( $children_elements[$element->$id_field] );
    
        // the rest of the method...
    }
    

    Overriding Walker::display_element() is the right move, but it’s better to actually address the problem at the root of the issue rather than simply tacking a class on at this point, for two reasons. First, the real problem isn’t a missing class but rather the un-patched bug in WordPress that Kikito noted: the problem is that $args[0] isn’t always an array. That appears to be the typically expected type for Walker::display_element(), but we’re actually dealing here with Walker_Nav_Menu::display_element(), and in that case args ends up being passed in as a standard object type rather than an array type. As such, we simply need to add the has_children element using object notation instead of array notation. Problem solved![1]

    Adding that elseif accounts for the ordinary Nav Menu case. This is the same form that will hopefully make it into the core class in this patch, at which point you’ll no longer have to extend it. They should probably patch it further to account for the case that $args[0] is neither an array nor an object, but I don’t expect to see that happen.

    Second, in order to keep good separation of concerns between the various methods, classes should really be added in the start_el() method or elsewhere, since display_element() isn’t doing any of the class handling.

    As a result, you can then override start_el() however you like: you can add your own custom classes, or ignore elements entirely, or supply custom text, or whatever you like. (In my case, I’m working around an existing Javascript menu implementation that has very specific classing requirements based on parents and children, so I can’t just add the same classes to everything that has a child – which is precisely why this separation of concerns matters.) In my code:

    function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
        $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
        $class_names = $value = '';
    
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;
    
        $has_children = (is_object($args) && $args->has_children) || (is_array($args) &&  $args['has_children']);
        if ($has_children) {
            // do whatever you need to do
        }
    
        // everything else the method does...
    }
    

    [1] This is of course one of the potential pitfalls of dynamically typed languages like PHP… it’s not a problem, as long as you’re careful. The WordPress developers weren’t careful here.

  8. If you don’t want the overhead of a hard query or function, you can do this in jQuery:

    (function() {
        // Add 'has_children' class to menus
        jQuery.each(jQuery('.menu-item').has('ul.sub-menu'), function() {
            jQuery(this).addClass('has_children');
        });
    })();
    
  9.     /**
         * @see My_Nav_Walk::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
         * @url:http://www.liyinqing.com
         */
    class My_Nav_Walk 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        ) .'"' : '';
    
        // Check our custom has_children property.here is the points
        if ( $args->has_children ) {
          $attributes .= ' class="menu parent"';
        }
    
        $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 );
      }
    
      function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) {
        $id_field = $this->db_fields['id'];
        if ( is_object( $args[0] ) ) {/.here is the points
          $args[0]->has_children = ! empty( $children_elements[$element->$id_field] );
        }
        return parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
      }
    

    }

  10. Since this question is one of the first results on Google search, has been referenced in other webpages and most the of the answers here are outdated I thought I would post this answer.

    start_el() has classes as part of $item object. So You can add this in start_el():

       if(in_array('menu-item-has-children', $item->classes) && $depth != 0)
       {
           // Your Code
       }
    

    Note that $depth is not required in the conditions but if removed your code will be applied to the first item (i.e. item with 0 depth).

    As for the compatibility part, the class ‘menu-item-has-children’ has been added to WordPress 3.7 (October 2013) and I have tested it on the latest WordPress 4.4 (as of the time of posting this answer).

  11. Instead of rewriting core functionality of the Walker_Nav_Menu or Walker class methods let’s just use good old class inheritance.

    The Code

    class Has_Child_Walker_Nav_Menu extends Walker_Nav_Menu {
        public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
            if ( ! $element ) {
                return;
            }
            $element->has_children = ! empty( $children_elements[ $element->{$this->db_fields['id']} ] );
            parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
        }
    }
    

    How It Works

    1. We create a new class Has_Child_Walker_Nav_Menu that inherits from Walker_Nav_Menu that in turn inherits from Walker.
    2. We override the Walker->display_element() method with our own function since it the first method called for each individual menu-items while we are walking the menu item tree.
    3. In our display_element() method we create a new property has_children on the $element a.k.a. menu item. This makes the property available on the $item object in both start_el() and end_el() methods, this means it also passed to the filters called by start_el().
    4. The value of our new has_children property value is calculated the same way the Walker instance property has_children is determined.
    5. Lastly we call the display_element() method on the parent Walker_Nav_Menu… well actually grandparent Walker since Walker_Nav_Menu doesn’t have it’s own display_element() method. We make sure to pass all the values from our display_element() method to the Walker->display_element() method.

    That’s it!

    Now we have $item->has_children available anywhere we might need it including filters.

    Example Usage

    The Menu

    wp_nav_menu(
        array(
            'theme_location'  => 'main_nav',
            'container_class' => 'main-nav',
            'walker'          => new Has_Child_Walker_Nav_Menu()
        )
    );
    

    The Filter

    function my_menu_dropdown( $output, $item, $depth, $args ) {
        if ( $item->has_children ) {
            $output .= '<a href="#" class="expand-menu-toggle"><i class="fal fa-angle-down"></i></a>';
        }
        return $output;
    }
    add_filter( 'walker_nav_menu_start_el', 'my_menu_dropdown', 10, 4 );
    
  12. Thanks for Start_el function, my function follow that function to run query.
    I have a function will count sub menu of parent menu by ID.

    function nav_count_children($parent_id){
        global $wpdb;
        $query = "SELECT COUNT(*) FROM $wpdb->postmeta 
                WHERE meta_key='_menu_item_menu_item_parent' 
                AND meta_value=$parent_id";
        $count_children = $wpdb->get_var( $query );
        return $count_children;
    }
    

    Run
    In foreach of wp_get_nav_menu_items function, select ID of parent Menu by $item->menu_item_parent ==0.

    It’s working for me and very simple.

  13. There is a simple solution source.

    function start_el(&$output, $item, $depth=0, $args=array()) {
    
      global $wpdb;
      $children_count = $wpdb->get_var(
        $wpdb->prepare("
          SELECT COUNT(*) FROM $wpdb->postmeta
          WHERE meta_key = %s
          AND meta_value = %d
        ", '_menu_item_menu_item_parent', $item->ID)
      );
    
      if( $children_count > 0 ) {
        // has children
        $classes[] = 'parent';
      }
    
      [...]
    
  14. use this simple code in you walker class

     class Description_Walker extends Walker_Nav_Menu
        {
        function start_el(  &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        global $wp_query;
        ////
    
        ur code in this part 
        ///////
         $depth_classes = array(
            ( $depth == 0 ? 'nav-item' : 'nav-submenu-item' ),
            ( $depth >=2 ? 'sub-sub-menu-item' : '' ),
            ( $depth % 2 ? 'menu-item-odd' : 'menu-item-even' ),
            'menu-item-depth-' . $depth
        );
        $depth_class_names = esc_attr( implode( ' ', $depth_classes ) );
    
        // passed classes
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $class_names = esc_attr( implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) ) );
    
    
         $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
        }
        }