Jump to content
Larry Ullman's Book Forums
banacan

Using Recursion To Create A Nested Navigation Menu

Recommended Posts

I'm using a modification of script 1.3 to create a nested navigation menu. So far I've got the structure working great, the question I have is how do I add another field to the list() array - or in my case the link() array. What I have now is the navigation name showing in the structure, but I need the value from another field for the link. Here is what I have:

 

// Funtion for displaying list.
// Receives one argument: an array.
function make_list ($parent) {

 // Need the main pages array:
global $links;

// Start an unordered list:
echo '<ul class="sf-menu sf-vertical">';

// Loop through each subarray:
foreach ($parent as $pid => $nav_name) {

// Display the list item:
echo "<li class=\"sfHover\"><a href=\"$link_name\">$nav_name</a>";

// Check for subpages:
if (isset($links[$pid])) {

 // Call this function:
 make_list($links[$pid]);

 }

 // Complete the list item:
 echo '</li>';

} // End of FOREACH loop.

// Close the unordered list:
echo '</ul>';

} // end of make_list() function
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Test nested navigation</title>
</head>
<body>
<?php

// Initialize the storage array:
$links = array();
while (list($page_id, $pid, $nav_name, $link_name ) = mysql_fetch_array($pageList, MYSQL_NUM)) {
// Add to the array:
$links[$pid][$page_id] = $nav_name;

}
// For debugging:
echo '<pre>' . print_r($links,1) . '</pre>';
// Send the first array element
// to the make_list() function:
make_list($links[$start]);

 

As you can see, I've added $link_name to the while(list()) but I can't understand how to get $link_name added to the array which contains the nested navigation items. You can also see how I have added $link_name to the <a> element in the <li> but when I test the script I only get <a href="">whatever</a>.

 

Here is my SQL:

 

mysql_select_db($database_siteuser, $siteuser);
$query_pageList = "SELECT page_id, pid, nav_name, link_name, ordr FROM webtext WHERE page_id != 0 AND navInclude = 'y' OR sidebar_disp = 'y' ORDER BY pid, ordr";
$pageList = mysql_query($query_pageList, $siteuser) or die(mysql_error());
$row_pageList = mysql_fetch_assoc($pageList);

 

Any help would be appreciated.

Share this post


Link to post
Share on other sites

You would replace this line:

$links[$pid][$page_id] = $nav_name;

with:

$links[$pid][$page_id] = array('nav' => $nav_name, 'link' => $link_name);

That will store the link-name as an array. Then, to display them, you would replace:

foreach ($parent as $pid => $nav_name) {

with:

foreach ($parent as $pid => $item) {

And within the foreach loop, you'd use $item['nav'] and $item['link']

Share this post


Link to post
Share on other sites

Hi Larry,

 

Thanks for the reply. I made those changes and the structure is correct (I see the sub-array with 'link' and 'nav') but I'm having difficulty with the output. If I have:

echo "<li class=\"sf_hover\">$nav_name"; I see the structure (with the debugging uncommented) though there is nothing but an empty ul li.

If I have:

echo "<li class=\"sf_hover\">$item['nav']"; I see nothing, just a blank page.

What is happening?

Share this post


Link to post
Share on other sites

As I suspected, the problem was with escaping single and double quotes. Instead of trying to find my way through that labyrinth, I just combined HTML and code using the concatenator ".", now all is well.

 

Thanks Larry.

Share this post


Link to post
Share on other sites

Hi Larry,

 

I have another question, is there a way to show the "path" of sub-navigation items within this script?

 

Say the main category is Fresh Fruits & Vegetables, a sub-category would be Fruits, and the item would be Apples. I would like to have the hierarchy part of the <li> element for linking purposes. So something like:

 

fresh-fruits-and-vegetables/fruits/apples/

 

In the multidimensional array structure the "categories" are represented by the pid, so I would need to use the associated link_name for each level or iteration. Can this be done?

 

Many thanks,

 

Brett

Share this post


Link to post
Share on other sites

Hmmm...interesting question. It certainly can be done, it's just a matter of how. My initial thought is to pass the preface (e.g., fresh-fruits-and-vegetables/fruits/) along as a second argument to the function, so that the function can add that to subcategories. You'd just need a little logic as to when the preface gets passed along and when it changes.

Share this post


Link to post
Share on other sites

The recursion function "walks the tree", iteration by iteration, mapping the entire tree. But as it does so, it replaces the variable value during each iteration. So how would the "preface" be created? It seems to me that what I need is a second function that can walk the tree in the opposite direction creating an array of all link_names related by pid and page_id up to 0.

 

As you have probably figured out, I'm trying to create a dynamic menu for use with my .htaccess file. Using your modularized web site approach, I have the home page at the site root, then I have sub-directories (what I call category directories) which contain modularized index pages (all category pages have a pid value of 0). All pages below each category page can be further nested by making the parent page (page_id) it is descended from the pid. Though the descendant category pages are really just the index page with the content being replaced, I need to show the hierarchical relationship to match the RewriteRule in my .htaccess file.

 

This is an issue that must be common, since all frameworks use this method. Do you know how they have solved this problem? I assume some form of recursion is necessary, since the level of depth is unknown.

 

Thanks for your continued help.

Share this post


Link to post
Share on other sites

You already have recursion, so you shouldn't need to do a reverse recursion, too. What I was thinking is you start by defining the recursive function so it takes an optional second argument:

function make_list ($parent, $preface = '') {

 

Then, when you call the function recursively, you can send the current item along as the preface of the subelements:

make_list($links[$pid], $nav_name);

 

And you change the function so that it uses the preface, when appropriate, in creating the link.

 

That's the basic idea. To acknowledge sub-sub links, you can append the next the next preface onto the current preface and pass that along. The only thing that I don't know the answer to off the top of my head is when/how you reset the preface. Perhaps you have thoughts on that or I might if I had a visual of the sample data.

Share this post


Link to post
Share on other sites

Hi Larry,

 

I'm sorry for the long delay in responding to your last post, I was sick for a week and when I got back to work I had a big backlog of work and looming deadlines.

 

I have tried a couple of things so far but nothing is working. I tried adding the second argument to the function, but when I called the function with $nav_name as the value of the second parameter, nothing at all appeared - there was no value.

 

I thought it might help for you to see the nesting structure so you can see what I'm trying to accomplish:

 

Array
(
   [0] => Array
    (
	    [34] => Array
		    (
			    [pid] => 0
			    [nav] => Mouldings
			    [link] => mouldings
		    )
	    [35] => Array
		    (
			    [pid] => 0
			    [nav] => News
			    [link] => news
		    )
	    [57] => Array
		    (
			    [pid] => 0
			    [nav] => Gallery
			    [link] => gallery
		    )
    )
   [34] => Array
    (
	    [36] => Array
		    (
			    [pid] => 34
			    [nav] => Historical
			    [link] => historical-mouldings
		    )
	    [37] => Array
		    (
			    [pid] => 34
			    [nav] => Stock
			    [link] => stock-mouldings
		    )
	    [46] => Array
		    (
			    [pid] => 34
			    [nav] => Custom
			    [link] => custom-mouldings
		    )
    )
   [35] => Array
    (
	    [44] => Array
		    (
			    [pid] => 35
			    [nav] => Industry
			    [link] => industry-news
		    )
    )
   [36] => Array
    (
	    [45] => Array
		    (
			    [pid] => 36
			    [nav] => Colonial
			    [link] => colonial-style-mouldings
		    )
	    [38] => Array
		    (
			    [pid] => 36
			    [nav] => Craftsman
			    [link] => craftsman-style-mouldings
		    )
	    [39] => Array
		    (
			    [pid] => 36
			    [nav] => Federal
			    [link] => federal-style-mouldings
		    )
	    [47] => Array
		    (
			    [pid] => 36
			    [nav] => Georgian & Wren
			    [link] => georgian-and-wren
		    )
	    [59] => Array
		    (
			    [pid] => 36
			    [nav] => Victorian
			    [link] => victorian-style-mouldings
		    )
    )
   [47] => Array
    (
	    [58] => Array
		    (
			    [pid] => 47
			    [nav] => Georgian
			    [link] => georgian-style-mouldings
		    )
	    [48] => Array
		    (
			    [pid] => 47
			    [nav] => Wren
			    [link] => wren-style-mouldings
		    )
    )
)
   Mouldings
    Historical
	    Colonial
	    Craftsman
	    Federal
	    Georgian & Wren
		    Georgian
		    Wren
	    Victorian
    Stock
    Custom
   News
    Industry
   Gallery

 

This is not all of the structure but it will show you the level of nesting involved.

 

Your continued help is appreciated.

Share this post


Link to post
Share on other sites

Hi Larry,

 

I've been able to get the parent link added to the path of the current link, but I still haven't figured out how to append the new parent link onto the previous "preface" and continue that process to the last child. Also, it's unclear to me how the recursion process works - I hope I can describe what I mean because I'm sure it makes very big difference. During recursion, does the "path" begin at the root level and proceed down every child to the end, creating a complete map for each child, or does it map only from the parent node for each? I'm assuming, based on your previous post about resetting, that it only maps from the parent node.

 

Here is what I have right now that is working - sort of:

 

// Funtion for displaying list.
// Receives one argument: an array.
function make_list ($parent, $preface = '') {

 // Need the main pages array:
global $links;

// Start an unordered list:
echo '<ul class="sf-menu sf-vertical">';

// Loop through each subarray:
foreach ($parent as $pid=>$item) {

// Display the list item:
?> <li class="sf-hover"><a href="<?php echo $basedir . "/" . $preface . "/" . $item['link'] ?>/"> <?php echo $item['nav'];

// Check for subpages:
if (isset($links[$pid])) {

 // Call this function:
 make_list($links[$pid], $item['link']);

 }

 // Complete the list item:
 echo '</li>';

} // End of FOREACH loop.

// Close the unordered list:
echo '</ul>';

} // end of make_list() function
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Test nested navigation</title>
</head>
<body>
<?php
// Initialize the storage array:
$links = array();
while (list($page_id, $pid, $nav_name, $link_name ) = mysql_fetch_array($pageList, MYSQL_NUM)) {
// Add to the array:
$links[$pid][$page_id] = array('pid'=>$pid, 'nav'=>$nav_name,'link'=>$link_name);

}
// For debugging:
echo '<pre>' . print_r($links,1) . '</pre>';
// Send the first array element
// to the make_list() function:
make_list($links[0]);
?>
</body>

 

Thanks for your help.

Share this post


Link to post
Share on other sites

Well, I've figured out how to append the parents to the child to make the links correct. EXCEPT, as you said, I haven't been able to figure out how to get it to reset.

 

Anyway, here is my modified code that at least creates the full path - for one branch of the tree:

 

// Funtion for displaying list.
// Receives one argument: an array.
function make_list ($parent, $preface = '') {

 // Need the main pages array:
global $links;

// Start an unordered list:
echo '<ul class="sf-menu sf-vertical">';

// Loop through each subarray:
foreach ($parent as $pid=>$item) {

// Display the list item:
?> <li class="sf-hover"><a href="<?php echo $basedir . "/" . $preface . $item['link'] . "/" ?>"> <?php echo $item['nav'];

// Check for subpages:
if (isset($links[$pid])) {

 // Call this function:
 make_list($links[$pid], ($preface .= $item['link'] . "/") );

 }

 // Complete the list item:
 echo '</li>';

} // End of FOREACH loop.

// Close the unordered list:
echo '</ul>';

} // end of make_list() function

 

Mouldings
	Historical
		Colonial
		Craftsman
		Federal
		Georgian & Wren
			Georgian
			Wren
		Victorian
	Stock
	Custom
News
	Industry
Gallery

 

So from the structure shown above, the link for Wren is:

http://www.whatever....tyle-mouldings/

 

Great, but the very next link in the list is Victorian and that link is:

http://www.whatever....tyle-mouldings/

 

but it should be:

http://www.whatever....tyle-mouldings/

 

but it's not, because I don't know how to reset the preface.

 

To give you a little more info on what is being produced by this code, the link for Stock is:

http://www.whatever....tock-mouldings/

 

it should be:

http://www.whatever....tock-mouldings/

 

Preface seems to reset itself on links above itself, but not adjacent to itself, which is why the News link is:

http://www.whatever....mouldings/news/

 

but should be:

http://www.whatever.com/news/

 

news is adjacent to mouldings and so is gallery - all three are at the same nesting level.

 

One last link to show you is for Gallery:

http://www.whatever....s/news/gallery/

 

Here you can see how all previous adjacent links are added to the path, though they shouldn't be. Anyway, I hope this gives you some ideas on how this can be corrected and you might have a suggestion for me.

 

Thanks

Share this post


Link to post
Share on other sites

Okay. Thanks for sharing where you're at. Give me a couple of days to look this over and see what I can suggest.

Share this post


Link to post
Share on other sites

Hi Larry,

 

I'm still struggling to figure out how to reset $preface and I'm hoping you can provide some guidance.

 

As I said in an earlier post, $preface resets itself when moving up to the parent level but it does not reset itself at the sibling level. It seems to me what I need to do is find the $key=>$value pair where the $key = the current $pid, then locate that $value in the $preface string so I can use substr() and stripos() to replace $preface. So my question is how do I search - within the foreach loop - for the $key=>$value pair where the $key = the current $pid?

Share this post


Link to post
Share on other sites

Sorry for the lack of reply. This has been on my to-reply list, I just haven't had time to get to it. I'll do my best to run some code and come up with a solution in the next few days.

Share this post


Link to post
Share on other sites

Hey Larry,

 

I know you are busy and I also know that you give your time out of the goodness of your heart, so I want to thank you for spending a little of that valuable time on me.

 

Warm regards,

 

Brett

Share this post


Link to post
Share on other sites

Hey Brett. I'm sorry. I've really meant to get to this, but I'm so behind in my own work and about to take off for the holidays.

 

I haven't tested this yet, but I think the solution would be to use an array for the prefaces. For each new level, you add another item to the end of the array. And you pass the array to the function with each recursive call. Then, after the last recursive function call and before the next one (i.e., after the make_list() call but before the end of the foreach), you would pop off the last element in the array. I'm not positive that will work, but that's what I would test next.

 

Apologies for the delay and for not having the time to test this theory before offering it up.

Share this post


Link to post
Share on other sites

Hey Larry,

 

Thanks for your reply, I totally understand about being behind on work, I too am overloaded at the moment which is why I'm so late in replying to you.

 

It seems to me, if I understand what you are recommending, lopping off the last element of the array will work if each nested item is only one level away. But what happens if one nav link is 4 levels deep and the very next nav link is only 1 level deep? I'd only be lopping off to the third level which would make the hierarchy incorrect and the link wrong. Does that make sense?

 

So I'm wondering, would there be a way to assign the depth level to each nav link and to the current depth level of the preface so that the array string could be cut back the right number of levels based on each nav link level? Could that work?

 

Thanks,

 

Brett

Share this post


Link to post
Share on other sites

No, lopping off the last element of the array would be done after the recursive call. So if you were to go down four levels, there would be four pops of the last array element off of the array before getting back to that top level.

Share this post


Link to post
Share on other sites

Hi Larry,

 

I'm very very close now thanks to your suggestion. Here is what I currently have:

 

// Funtion for displaying links.
// Receives one argument: an array.
function make_list ($parent, $preface = '') {

 // Need the main pages array:
global $links;

// Start an unordered list:
echo '<ul class="sf-menu sf-vertical">';

// Loop through each subarray:
foreach ($parent as $pid=>$item) {

// Display the list item:
?> <li class="sf-hover"><a href="<?php echo $basedir . "/" . $preface . $item['link'] . "/" ?>"> <?php echo $item['nav']; ?></a><?php

// Check for subpages:
if (isset($links[$pid])) {

 // Call this function:
 make_list($links[$pid], ($preface .= $item['link'] . "/") );

 // lop off last element of $preface array
 $length = strlen($preface);
 $pos = strrpos($preface,'/', -2);
 $slength = ($length - 1) - $pos;
 $preface = substr($preface, 0, -$slength);

 }
 // Complete the list item:
 echo '</li>';

} // End of FOREACH loop.

// Close the unordered list:
echo '</ul>';

} // end of make_list() function

// Initialize the storage array:
$links = array();
while (list($page_id, $pid, $nav_name, $link_name ) = mysql_fetch_array($pageList, MYSQL_BOTH)) {
// Add to the array:
$links[$pid][$page_id] = array('pid'=>$pid, 'nav'=>$nav_name,'link'=>$link_name);

}

 

Here is the nav menu output:

 

Mouldings - link path is: /mouldings/
	Historical - link path is: /mouldings/historical-mouldings/
		Colonial - link path is: /mouldings/historical-mouldings/colonial-style-mouldings/
		Craftsman - link path is: /mouldings/historical-mouldings/craftsman-style-mouldings/
		Federal - link path is: /mouldings/historical-mouldings/federal-style-mouldings/
		Georgian & Wren - link path is: /mouldings/historical-mouldings/georgian-and-wren/
			Georgian - link path is: /mouldings/historical-mouldings/georgian-and-wren/georgian-style-mouldings/
			Wren - link path is: /mouldings/historical-mouldings/georgian-and-wren/wren-style-mouldings/
		Victorian - link path is: /mouldings/historical-mouldings/victorian-style-mouldings/
	Stock - link path is: /mouldings/stock-mouldings/
	Custom - link path is: /mouldings/custom-mouldings/
News - link path is: /mnews/
	Industry - link path is: /mnews/industry-news/
Gallery - link path is: /mgallery/

 

As you can see, the links are all correct until News and Gallery where there is a stray "m" mucking things up. These are base categories like Mouldings, but because I had to subtract 1 from $length to have the deeper links display correctly, it leaves the stray "m" (from Mouldings) in front of News and Gallery. Here is what happens if I don't subtract 1 from $length:

 

Mouldings - link path is: /mouldings/
	Historical - link path is: /mouldings/historical-mouldings/
		Colonial - link path is: /mouldings/historical-mouldings/colonial-style-mouldings/
		Craftsman - link path is: /mouldings/historical-mouldings/craftsman-style-mouldings/
		Federal - link path is: /mouldings/historical-mouldings/federal-style-mouldings/
		Georgian & Wren - link path is: /mouldings/historical-mouldings/georgian-and-wren/
			Georgian - link path is: /mouldings/historical-mouldings/georgian-and-wren/georgian-style-mouldings/
			Wren - link path is: /mouldings/historical-mouldings/georgian-and-wren/wren-style-mouldings/
		Victorian - link path is: /mouldings/historical-mouldingsvictorian-style-mouldings/
	Stock - link path is: /mouldingsstock-mouldings/
	Custom - link path is: /mouldingscustom-mouldings/
News - link path is: /news/
	Industry - link path is: /news/industry-news/
Gallery - link path is: /gallery/

 

You will see that the "/" is missing between historical-mouldings and victorian-style-mouldings, as well as mouldings and stock-mouldings, and mouldings and custom-mouldings. Now however, News and Gallery are correct. Do you have any ideas how to solve this?

 

Thanks for your continued help.

Share this post


Link to post
Share on other sites

I think I finally have it figured out. I had forgotten that I had access to the pid value for each item - which for category pages is 0 - so for those whose pid = 0 I reset the $preface manually, the rest I kept as before.

 

Here is my final code for those who are interested:

 

// Funtion for displaying links.
// Receives one argument: an array.
function make_list ($parent, $preface = '') {

 // Need the main pages array:
global $links;

// Start an unordered list:
echo '<ul class="sf-menu sf-vertical">';

// Loop through each subarray:
foreach ($parent as $pid=>$item) {

// Display the list item:
?> <li class="sf-hover"><a href="<?php echo $basedir . "/" . $preface . $item['link'] . "/" ?>"> <?php echo $item['nav']; ?></a><?php

// Check for subpages:
if (isset($links[$pid])) {

 // Call this function:
 make_list($links[$pid], ($preface .= $item['link'] . "/") );

 // lop off last element of $preface array or reset it
 $length = strlen($preface);
 $pos = strrpos($preface,'/', -2);
 $slength = ($length - 1) - $pos;
 $item['pid'] == 0 ? $preface = '' : $preface = substr($preface, 0, -$slength);

 }
 // Complete the list item:
 echo '</li>';

} // End of FOREACH loop.

// Close the unordered list:
echo '</ul>';

} // end of make_list() function

// Initialize the storage array:
$links = array();
while (list($page_id, $pid, $nav_name, $link_name ) = mysql_fetch_array($pageList, MYSQL_BOTH)) {
// Add to the array:
$links[$pid][$page_id] = array('pid'=>$pid, 'nav'=>$nav_name,'link'=>$link_name);

}

 

Here is the resulting nav menu:

 

Mouldings - link path is: /mouldings/
	Historical - link path is: /mouldings/historical-mouldings/
		Colonial - link path is: /mouldings/historical-mouldings/colonial-style-mouldings/
		Craftsman - link path is: /mouldings/historical-mouldings/craftsman-style-mouldings/
		Federal - link path is: /mouldings/historical-mouldings/federal-style-mouldings/
		Georgian & Wren - link path is: /mouldings/historical-mouldings/georgian-and-wren/
			Georgian - link path is: /mouldings/historical-mouldings/georgian-and-wren/georgian-style-mouldings/
			Wren - link path is: /mouldings/historical-mouldings/georgian-and-wren/wren-style-mouldings/
		Victorian - link path is: /mouldings/historical-mouldings/victorian-style-mouldings/
	Stock - link path is: /mouldings/stock-mouldings/
	Custom - link path is: /mouldings/custom-mouldings/
News - link path is: /news/
	Industry - link path is: /news/industry-news/
Gallery - link path is: /gallery/

 

Thank you Larry for your help, I'm sure I wouldn't have gotten this figured out without it. And thank you also for the fine book that provided me with the initial script which has made this possible. This was a major accomplishment for me because it was vital for a dynamic navigation menu which works with my .htaccess file. I sure do value your books and this forum where you are always so generously giving of your time.

 

Best,

 

Brett

Share this post


Link to post
Share on other sites

Kudos for figuring it out and thanks for sharing your solution. I'm glad you got it working. Thanks, too, for the nice words on what I do!

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×