Menu items, pages and (hierarchical) taxonomies are all examples of data with a tree like structure: terms can have parents, children and siblings. Usually we would like to reflect this structure in the HTML markup. For displaying a menu, for instance, we want the HTML to be of a list of 'top level' links, with nested lists of their children, which themselves contain nested lists of their children, and so on. This tutorial will guide you through a class WordPress provides which makes producing this mark-up extremely simple.
What Is the Walker Class?
The walker class is an abstract class designed to help traverse and display elements which have a hierarchical (or tree like) structure. It doesn't actually 'do' (in the sense of generating HTML) anything. It simply traces each branch of your tree: it has to be extended by other classes which tell it what to do for each element it comes across. WordPress provides its own extending classes, such as:
-
Walker_Nav_Menu
– for displaying the HTML for navigation menus -
Walker_Page
– for displaying a list of pages -
Walker_Category
– for displaying a list of taxonomy terms.
Each of these classes extend the Walker class by simply dictating what the class outputs at each element and level of the tree. In order to de-mystify this class we shall look at its main methods and a couple of examples of how to use it. The class itself can be found here.
Walking the Tree
Walk
walk( $elements, $max_depth)
The walker class gets kicked off with the walk method and it's this method which returns the HTML once it's been generated. It accepts two arguments:
- An array of elements that we wish to display, which will have some sort of parent-child relationship
-
$max_depth
– sets how many generations we explore - Ok 3... If you scratch the surface of this method you'll find you can actually pass extra arguments which get gathered into an array:
$args
. This is then passed to other methods in the class
The walk method singles out the 'top level' elements – those without parents – and places them in one array. The rest, the children, are placed in a second array where the key is the ID of its parent (it's a two dimensional array as one parent can have multiple children):
$children_elements = array( '1' => array() //Array of elements corresponding to children of 1, '4' => array() //Array of elements corresponding to children of 4 );
It then loops through each of the parent elements in turn and applies the method display_element
.
Display_Element
display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output )
As the name suggests display_element
is responsible for displaying an element in our tree. In fact, it calls several functions to do this. These functions are deliberately left blank in the Walker class – and it's these which are altered in the extending classes, as they determine the actual HTML returned. These include:
-
start_lvl
– a function to return the HTML for the start of a new level. In the case of lists, this would be the start of a new 'sub-list', and so would be responsible for returning the<ul>
tag -
end_lvl
– called when we have finished a level. In the navigation menu example, this function is responsible for ending the sub-list with a closing list tag</ul>
-
start_el
– the function responsible for displaying the current element we are on. In the case of menus, this means the<li>
tag and the item's link. -
end_el
– the function called after an element and all it's children have been displayed. For our menu example this means returning a closing</li>
tag.
So what does display_element
actually do? It's actually where all the magic of the Walker class takes place. First lets take a look at what arguments it's given:
-
$element
– this is the element we are currently at on our tree -
$children_elements
– an array of all child elements (not just children of the element referred to above). This is the second array formed in thewalk
method and the keys are the IDs of the parent. -
$max_depth
– how far down we are allowed to explore -
$depth
– how far down we currently are -
$args
– optional arguments (mentioned earlier) -
$output
– The HTML thus far. This is added to as we explore more of the tree.
The display_element
method first calls start_el
which is responsible for displaying the element. Exactly how it does that depends on the context. For a drop-down menu it may be <select> Current Item
or for a navigation menu it may <li> Current Item
. Notice that there is no closing tag yet. If this element has children, we need to display them first so to that they are nested inside this item...
So next it checks if the current element we are on has any children and that we have not reached the maximum depth. If so, we explore each of the children in turn, by calling display_element
for each of them (with the depth argument incremented by one). In this way the display_element
recursively calls itself until we reach the bottom.
Suppose we have reached the 'bottom' (an element with no children or the maximum depth), then it calls end_el
which adds the closing tag. There the current instance of display_element
finishes and we move back up to the parent who applies display_element
to the next child, until we've processed each of its children. When the parent has no more children left we move back up the tree, and so on until every branch is explored. Confused? He's a diagram which I hope will clarify things:
Using the Walker Class: A Simple Example
Using the Walker class makes displaying custom hierarchal data very simple. Suppose you have an array of objects, with 'label
', 'parent_id
' and 'object_id
' properties that you wish to display a list of. This can now be easily be accomplished with a very simple class:
Note: The extending class is responsible for setting where to find an element's ID and that of its parent.
class Walker_Simple_Example extends Walker { // Set the properties of the element which give the ID of the current item and its parent var $db_fields = array( 'parent' => 'parent_id', 'id' => 'object_id' ); // Displays start of a level. E.g '<ul>' // @see Walker::start_lvl() function start_lvl(&$output, $depth=0, $args=array()) { $output .= "\n<ul>\n"; } // Displays end of a level. E.g '</ul>' // @see Walker::end_lvl() function end_lvl(&$output, $depth=0, $args=array()) { $output .= "</ul>\n"; } // Displays start of an element. E.g '<li> Item Name' // @see Walker::start_el() function start_el(&$output, $item, $depth=0, $args=array()) { $output. = "<li>".esc_attr($item->label); } // Displays end of an element. E.g '</li>' // @see Walker::end_el() function end_el(&$output, $item, $depth=0, $args=array()) { $output .= "</li>\n"; } } $elements=array(); // Array of elements echo Walker_Simple_Example::walk($elements);
Using the Walker Class: Advanced Example
You can extend the walker classes to change what content is displayed, alter the HTML generated or even prevent certain branches from being shown. Functions such as:
Provide an option to specify your own custom Walker class – allowing you to alter their appearance with relative ease by specifying your own custom walker class. In many instances it is actually easier to extend an appropriate walker extension, rather than the Walker class itself.
Suppose you want to have a secondary (sub) menu that is related to your primary menu. This may take the form of links that sit just below your primary menu or in a side-bar which show only the 'descendant' menu items of the current 'top-level page'. As an example from the diagram above, if we're on the 'Archive', 'Author', or 'News' sub pages, we would like to show all of the links below 'Archive'. Since Walker_Nav_Menu
does most of what we want, we shall extend that class rather than the Walker class. This saves us a lot of effort, since the Walker_Nav_Menu
adds the appropriate classes ('current
', 'current-ancestor
' etc) to the relevant links. We shall extend the Walker_Nav_Menu
walker class to alter the logic slightly, and prevent it from displaying any top-level links or any of the descendants of the 'non-root' pages.
Some Ground Work: Theme Locations
First of all, in your template files, we will use the wp_nav_menu()
function twice, pointing to the same theme location (I shall call it 'primary
'). If you don't have a theme location registered already you should read this article. Whichever theme location you are using, you should save a menu to that location. We shall display this menu twice. First, wherever you want your 'top-level' menu to appear:
wp_nav_menu( array('theme_location'=>'primary','depth' => 1) );
Then again, with a custom walker, to display only the (relevant) child pages.
wp_nav_menu( array('theme_location'=>'primary','walker' => new SH_Child_Only_Walker(),'depth' => 0) );
Extending the Walker
First of all we don't want to display top-level parents. Recall that the function responsible for the opening <li>
tag and the link is start_el
and the function responsible for the closing </li>
tag is end_el
. We simply check if we are at the parent level. If we are, we don't do anything. Otherwise, we continue 'as normal' and call the function from the Walker_Nav_Menu
class.
// Don't print top-level elements function start_el(&$output, $item, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::start_el(&$output, $item, $depth, $args); } function end_el(&$output, $item, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::end_el(&$output, $item, $depth, $args); }
We extend the display_element
. This function is responsible for traveling down the branches. We want to stop it in its tracks if we are at the top-level and not on the current root link. To check if the branch we are on is 'current', we check if the item has any of the following classes: 'current-menu-item
', 'current-menu-parent
', 'current-menu-ancestor
'.
// Only follow down one branch function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) { // Check if element as a 'current element' class $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' ); $current_class = array_intersect( $current_element_markers, $element->classes ); // If element has a 'current' class, it is an ancestor of the current element $ancestor_of_current = !empty($current_class); // If this is a top-level link and not the current, or ancestor of the current menu item - stop here. if ( 0 == $depth && !$ancestor_of_current) return; parent::display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ); }
We now extend the start_lvl
and end_lvl
functions. These are responsible for outputting the HTML that wraps a level (in this case the <ul>
tags). If we are on the top level we don't want to display these tags (after all the contents will not be shown).
// Don't wrap the top level function start_lvl(&$output, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::start_lvl(&$output, $depth, $args); } function end_lvl(&$output, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::end_lvl(&$output, $depth, $args); }
That class in full:
class SH_Child_Only_Walker extends Walker_Nav_Menu { // Don't start the top level function start_lvl(&$output, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::start_lvl(&$output, $depth,$args); } // Don't end the top level function end_lvl(&$output, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::end_lvl(&$output, $depth,$args); } // Don't print top-level elements function start_el(&$output, $item, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::start_el(&$output, $item, $depth, $args); } function end_el(&$output, $item, $depth=0, $args=array()) { if( 0 == $depth ) return; parent::end_el(&$output, $item, $depth, $args); } // Only follow down one branch function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) { // Check if element as a 'current element' class $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' ); $current_class = array_intersect( $current_element_markers, $element->classes ); // If element has a 'current' class, it is an ancestor of the current element $ancestor_of_current = !empty($current_class); // If this is a top-level link and not the current, or ancestor of the current menu item - stop here. if ( 0 == $depth && !$ancestor_of_current) return parent::display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ); } }
Once you understand how the walker class works you can extend it (or WordPress' existing extensions) to alter how your hierarchal data is displayed. For instance you can:
- Include descriptions with menu links or category descriptions.
- Exclude whole branches of a menu for logged-out users.
- Include post meta in your list of pages.
Comments