Jump to content
apeisa

Recursive navigation

Recommended Posts

Some folks here have probably developed few different ways for recursive navigation. I was just starting to write my own, but thought that I ask from here first.

Looking for something like this:

- Products

- Services

- People

-- Management

-- HR

---- Matti Meikäläinen

---- Jaana Jallakkala

---- Will Ferrell

-- Marketing

-- Pets

- Contact us

Share this post


Link to post
Share on other sites

Are you looking for something that generates a nested list from current page down to homepage?  That's what it looks like, but I just wanted to double check.

Share this post


Link to post
Share on other sites

Output could be something like this:

<ul>
 <li><a>Products</a></li>
 <li><a>Services</a></li>
 <li class="on-path"><a>People</a>
   <ul>
      <li class="here"><a>Someone</a></li>
   </ul>
 </li>
 <li><a>Contact Us</a></li>
</ul>

Share this post


Link to post
Share on other sites

Sounds good. I will put together a code sample. I'm leaving to pick up my daughter from school now, so may not be back online for awhile. But I will post as soon as I can.

Share this post


Link to post
Share on other sites

I wanted to try coding it, and turned out pretty simple. Of course the code could be cleaner (and I could be a better programmer ;)), but it is a start. There is one TODO: that I couldn't get right. Our current cms puts level-n classes for css-styling and wanted to add that into markup too, but couldn't find a way to get last item on loop.

class simpleNav {

/**
 * Holds the current page
 *
 */
public $page;

/**
 * Root page, this won't be rendered, but pages below this will be
 *
 */
public $rootPage;

/**
 * Keeps count of the current depth
 *
 */
public $depth;

/**
 * Unique id for the most outer ul element
 *
 */
public $id;

/**
 * Holds page id:s of all the pages on path
 *
 */
private $pathIdArray;

public function __construct($page, $rootPage = null, $depth = 1, $id = null)
{
	$this->page = $page;
	$this->pathIdArray = $this->_getPathNav($page);
	$this->rootPage = $page->rootParent;
	$this->depth = $depth;
	$this->id = $id;
	if(!$rootPage) $this->rootPage = $page->rootParent;
}

public function render()
{
	if ($this->rootPage->numChildren > 0) {

		// Output the ul and id only if it's given
		echo "\n<ul class='navi level-$this->depth'";
		if($this->id) {
			echo " id='$this->id'";
			$this->id = null; // we want to give ID only for the first ul
		}
		echo ">";

		$loopedPages = $this->rootPage->children;

		foreach($loopedPages as $child) {
			$class = "level-$this->depth";

			// add "here" class if the current page is active
			if ($this->page === $child) $class .= " here";

			// check if looped page is one of the parents of current page (or is current page)
			if (in_array($child->id, $this->pathIdArray)) $class .= " on-path";

			echo "\n\t<li class='{$class}'>\n\t\t<a href='{$child->url}'>{$child->title}</a>";
			if (in_array($child->id, $this->pathIdArray)) {
				$this->depth++;
				$this->rootPage = $child;
				$this->render();
			}
			// TODO: If last element, $this->depth--
		}
		echo "\n</ul>";
	}
}

private function _getPathNav() {
foreach($this->page->parents as $parent) {
	$this->pathIdArray[] = $parent->id;
}
	$this->pathIdArray[] = $this->page->id;
	return $this->pathIdArray;
}

}

Usage is pretty simple:

$navi = new simpleNav($page);
$navi->render();

Share this post


Link to post
Share on other sites

I would solve it with something like this:

function menuLevel($menuPage, $maxLevel, $currentLevel = 0, $args=''){
 if (!$menuPages->numChildren()) return;
 
 $retStr = array();
 $retStr[] = '<ul '.($currentLevel===0?$args:'').'>';
 foreach($menuPage->children() as $p){
   $retStr[] = '<li><a href="'.$p->url.'">'.$p->title.'</a>';
   if ($maxLevel > $currentLevel)
     $retStr[] = menuLevel($p, $maxLevel, ++$currentLevel);
   $maxLevel = '</li>';
 }
 $retStr[] = '</ul>';
 
 return implode("\n",$retStr);
}

echo menuLevel($page->find('root'), 2, 0, 'id="navigation"');

But also, I'm weird.

untested. written in browser.

Edit:

Antti, last page in foreach (simplest)

 $count = count($pagesArray); //this depends: custom pages array or just children of one page?
 $count = $menuRootPage->numChildren();
 $i=0;
 foreach(...){
   if (++$i === $count) ... //last element
 }

Share this post


Link to post
Share on other sites

Looks like great solutions. Here's one more:

<?php

function treeMenu(Page $page = null, Page $rootPage = null) {

        if(is_null($page)) $page = wire('page');
        if(is_null($rootPage)) $rootPage = wire('pages')->get('/');

        $out = "\n<ul>";

        $parents = $page->parents;

        foreach($rootPage->children as $child) {
                $class = '';
                $s = '';

                if($child->numChildren && $parents->has($child)) {
                        $class = 'on_parent';
                        $s = str_replace("\n", "\n\t\t", treeMenu($page, $child));

                } else if($child === $page) {
                        $class = "on_page";
                        if($page->numChildren) $s = str_replace("\n", "\n\t\t", treeMenu($page, $page));
                }

                if($class) $class = " class='$class'";
                $out .= "\n\t<li>\n\t\t<a$class href='{$child->url}'>{$child->title}</a>$s\n\t</li>";
        }

        $out .= "\n</ul>";
        return $out;
}

Usage:

<?php

// no params: print out tree menu from current page to root parent
echo treeMenu(); 

// or specify what you want it to display
echo treeMenu($page, $page->rootParent);

Share this post


Link to post
Share on other sites

Thanks guys.

I modified few lines on Ryan's snippet (to add level-n to class):

<?php

function treeMenu(Page $page = null, Page $rootPage = null) {

        if(is_null($page)) $page = wire('page');
        if(is_null($rootPage)) $rootPage = wire('pages')->get('/');

        $out = "\n<ul>";

        $parents = $page->parents;

        foreach($rootPage->children as $child) {
                $class = "level-" . count($child->parents);
                $s = '';

                if($child->numChildren && $parents->has($child)) {
                        $class .= " on_parent";
                        $s = str_replace("\n", "\n\t\t", treeMenu($page, $child));

                } else if($child === $page) {
                        $class .= " on_page";
                        if($page->numChildren) $s = str_replace("\n", "\n\t\t", treeMenu($page, $page));
                }

                $class = " class='$class'";
                $out .= "\n\t<li>\n\t\t<a$class href='{$child->url}'>{$child->title}</a>$s\n\t</li>";
        }

        $out .= "\n</ul>";
        return $out;
}

Works nicely! Actually learned few new tricks from this also.

Reason for this script is that I want to build generic "default site" that can be used pretty much "out of the box". Of course good only for simple and generic sites, but I need something that sales guys can demo and let clients to play with. Of course this can be useful in other cases too.

  • Like 1

Share this post


Link to post
Share on other sites

Looks good! I didn't do a lot of testing with the function I posted previously, so if you find any bugs that aren't easy to resolve, just let me know and I'll fix them.

Share this post


Link to post
Share on other sites

This is so common - I do things like this all the time, not just for menus.

Does anyone else think this should be part of the core?

For now, I wrote a simple flat function:

/**
 * Recursive traverse and visit every child in a sub-tree of Pages.
 *
 * @param Page $parent root Page from which to traverse
 * @param callable $enter function to call upon visiting a child Page
 * @param callable|null $exit function to call after visiting a child Page (and all of it's children)
 */
function visit(Page $parent, $enter, $exit=null)
{
    foreach ($parent->children() as $child) {
        call_user_func($enter, $child);
        
        if ($child->numChildren > 0) {
            visit($child, $enter, $exit);
        }
        
        if ($exit) {
            call_user_func($exit, $child);
        }
    }
}

With PHP 5.3 you can generate a menu (or whatever else) recursively as simple as this:

visit(
    $pages->get('/menus/main')
    ,
    function(Page $page) {
        echo '<li><a href="' . $page->url . '">' . $page->title . '</a>';
        if ($page->numChildren > 0) {
            echo '<ul>';
        }
    }
    ,
    function(Page $page) {
        echo '</li>';
        if ($page->numChildren > 0) {
            echo '</ul>';
        }
    }
);

Dead simple.

I've seen the options for modules etc. that generate menus - they seem to grow out of control with a million options, and my own helpers seem to evolve the same way, and it doesn't jive with the beautiful, self-contained, simple templates you normally get away with in PW.

Would it make sense to have a standard visit() method in Page in the core?

  • Like 4

Share this post


Link to post
Share on other sites

Admittedly I don't use a treeMenu type function all that often myself. I think it just depends on the sites you are building. But I really like the solutions you've mentioned here--very elegant. Perhaps we should have some kind of visit() function like this in the core. Though unless it would be something most people would use, it may belong as a module or library. I'd be curious what other people think. 

Btw, I thought you might like the planned roadmap for ProcessWire 2.4. I'm hoping to finally get started on the full transition to PHP 5.3 here in the next couple of weeks, after PW 2.3 is final. I'm also going to try and get that alternate module configuration method in there for you, if you don't beat me to it. 

  • Like 3

Share this post


Link to post
Share on other sites

Admittedly I don't use a treeMenu type function all that often myself. I think it just depends on the sites you are building. But I really like the solutions you've mentioned here--very elegant. Perhaps we should have some kind of visit() function like this in the core. Though unless it would be something most people would use, it may belong as a module or library. I'd be curious what other people think. 

In my experience, 2 out of 3 sites need recursive navigation somewhere.

What do you think about adding a $selector argument to the function? So as to allow recursively visiting pages of a particular type, etc.

Edit: I also wonder if it's possible to preload the entire sub-tree to prevent high number of recursive queries?

Share this post


Link to post
Share on other sites

9 out of 10 of our sites need recursive navigation and Soma's module solves 98% of those needs.

Visit function on the core: I like the idea - though really hard to say how much use it would get. 

Share this post


Link to post
Share on other sites
What do you think about adding a $selector argument to the function? So as to allow recursively visiting pages of a particular type, etc.

Seems like a good idea!

Edit: I also wonder if it's possible to preload the entire sub-tree to prevent high number of recursive queries?

ProcessWire caches a lot of this stuff behind the scenes. Once a page is loaded, it doesn't reload it unless the memory cache gets cleared. Likewise, results of selector queries are cached as well. So the navigation tree would have to be pretty large before preloading would help much. But it is feasible to do. It would mean SQL querying the pages_parents table and pulling out all IDs, then preloading those pages with $pages->getById(array(ids...)). But I'm not sure it would amount to a measurable difference or not. 

Share this post


Link to post
Share on other sites

what about implementing the closure table pattern for specific templates and only for branches/leaves sharing the same template, as in trees of categories?

Share this post


Link to post
Share on other sites

Dead simple.

I've seen the options for modules etc. that generate menus - they seem to grow out of control with a million options, and my own helpers seem to evolve the same way, and it doesn't jive with the beautiful, self-contained, simple templates you normally get away with in PW.

Would it make sense to have a standard visit() method in Page in the core?

Hi,

this works fine for me - but how can I get the "home" in this menu?

thanks for helping!

Share this post


Link to post
Share on other sites

Something like this should work:

$menu_items->prepend($home);

Of course this all depends on the array of pages that you are iterating through to generate your menu. And of course you need to define $home, like:

$home = $pages->get("/");

Lots of options really.

Share this post


Link to post
Share on other sites

Something like this should work:

$menu_items->prepend($home);

Thank you! I tried this in the code above from mindplay.dk but I don´t get it work in the function call "visit".

I tried this:

     $home = $pages->get('/');
     $menu_items->prepend($home);
    
        visit(
            $menu_items
            ,
            function(Page $page) { 
            ...

But I got this error:

Error: Call to a member function prepend() on a non-object

Share this post


Link to post
Share on other sites

Thank you! I tried this in the code above from mindplay.dk but I don´t get it work in the function call "visit".

I tried this:

     $home = $pages->get('/');
     $menu_items->prepend($home);
    
        visit(
            $menu_items
            ,
            function(Page $page) { 
            ...

But I got this error:

Error: Call to a member function prepend() on a non-object

Guenter have you set $menu_items?

Share this post


Link to post
Share on other sites

Guenter have you set $menu_items?

hm - what exactly do you mean? Please see the code above in Post #11 and my changes made above, I made nothing more.

Share this post


Link to post
Share on other sites

Sorry I didn't actually look at mindplay's code. It might be easiest to just:

echo '<li><a href="/">Home</a></li>';

before the spot where you return the output of the function.

Perhaps if you are still confused so us your complete code so far.

  • Like 2

Share this post


Link to post
Share on other sites

It might be easiest to just:

echo '<li><a href="/">Home</a></li>';

before the spot where you return the output of the function.

That was my first simple solution, but I think it is better in the function or foreach loop.

Here my full code:

<?php
// function für Menü
function visit(Page $parent, $enter, $exit=null)
{
    foreach ($parent->children() as $child) {
        call_user_func($enter, $child);
        // wir müssen jene Kinder ausklammern, die nicht wirklich Untermenüs sind!
        // no childs that are not really childs (submenus) or such I dont want!
        if ($child->numChildren > 0 && $child->title !== 'Zimmer'  && $child->title !== 'Ferienwohnungen') {
            visit($child, $enter, $exit);
        }
        
        if ($exit) {
            call_user_func($exit, $child);
        }
    }
}	
?>
        <nav id="navigation" role="navigation">
          <div id="main-menu">
            <!-- <ul><li><a href="/">Home</a></li> --> <!-- dont want it here -->
             <?php 
                visit(
                    $pages->get('/')
                    ,
                    function(Page $page) {
                        //echo '<li><a href="' . $page->url . '">' . $page->title . '</a>';
                        // make the current page and only its first level parent have an active class
                        if($page === wire("page")){
                            $class .= ' active';
                        } 
                        /*if($page === wire("page")->rootParent || wire("page")->parents->has($child)){
                            $class .= ' active';
                        }*/
						$class = strlen($class) ? " class='".trim($class)."'" : '';
        
						echo "<li><a$class href='$page->url'>$page->title</a>";
                        if ($page->numChildren > 0) {
                            echo '<ul>';
                        }
                    }
                    ,
                    function(Page $page) {
                        echo '</li>';
                        if ($page->numChildren > 0) {
                            echo '</ul>';
                        }
                    }
                ); 
			 ?>
            </ul>
          </div>
        </nav>

Share this post


Link to post
Share on other sites

I don't really understand why you want it in the foreach loop. Unless you are rendering out this menu in multiple places, there is no need to have the code in the function either. Maybe that is just my take though? Curious to hear what others think.

Share this post


Link to post
Share on other sites

Unless you are rendering out this menu in multiple places, there is no need to have the code in the function either.

Thanks for answer! You are right! But "why simply, even if you can have it with difficulty..."  (I hope it is in english what I mean in german...) :)

Now, just for interest. I think it would be a cleaner codedesign.

As it is a code from mindplay.dk I still hope he maybe give a solution to that.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...