views:

259

answers:

7

The question: How do I generate navigation, allowing for applying different classes to different sub-items, from a multi-dimensional array?

Here is how I was doing it before I had any need for multi-level navigation:

Home 
Pics 
About

and was generated by calling nav():

function nav(){       
    $links = array(
        "Home" => "home.php",
        "Pics" => "pics.php",
        "About" => "about.php"
    );

    $base = basename($_SERVER['PHP_SELF']);

    foreach($nav as $k => $v){
        echo buildLinks($k, $v, $base);
    }
}

Here is buildLinks():

function buildLinks($name, $page, $selected){
    if($selected == $page){
       $theLink = "<li class=\"selected\"><a href=\"$page\">$name</a></li>\n";
    } else {
       $thelink = "<li><a href=\"$page\">$name</a></li>\n";
    }

    return $thelink;
}

My question, again:

how would I achieve the following nav (and notice that the visible sub navigation elements are only present when on that specific page):

Home
    something1
    something2 
Pics 
About

and...

Home
Pics
    people
    places 
About

What I've tried

From looking at it it would seem that some iterator in the SPL would be a good fit for this but I'm not sure how to approach this. I have played around with RecursiveIteratorIterator but I'm not sure how to apply a different style to only the sub menu items and also how to only show these items if you are on the correct page.

I built this array to test with but don't know how to work with the submenu1 items individually:

$nav = array(
array(
"Home" => "home.php",
"submenu1" => array(
    "something1"=>"something1.php",
    "something2" => "something2.php")
),
array("Pics" => "pics.php"),
array("About" => "about.php")
);

The following will print out the lot in order but how do I apply, say a class name to the submenu1 items or only show them when the person is on, say, the "Home" page?

$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($nav));

foreach($iterator as $key=>$value) {
    echo $key.' -- '.$value.'<br />';
}

And this gets me:

Home
something1
something2
Pics
About

But I have no way to apply classes to those sub items and no way to only display them conditionally because I don't see how to target just these elements.

+1  A: 

It seems like you might want to do this in a more object oriented way. If not, it seems like you should at least define an algorithm that makes sense, right now you are just blindly guessing. Instead, DEFINE.

For example:

I am defining my navigation to be a php hash based tree. A navigation item will have the following:

A) if there is a top level link, the array hash will contain an item(sub array) labeled "navigation leaf"

b) A navigation Leaf will contain elements labeled "Display value", "link value", and "alt value". These items will be used to generate an anchor tag.

c) if an element has a submenu, in addition to containing a "Navigation Leaf", a "subnavigation" element will be present. A subnavigation element will have a "Navigation Leaf" if it has a displayable navigation item.

You can then write functions/methods that will display your navigation based on the definition you choose.

Zak
+1  A: 

What I would do, is something along these lines:

class MenuItem {
    protected $active = false;
    protected $children = array();
    protected $name = '';
    protected $link = '';

    public function __construct($name, $link, $active) {}

    public function __toString() {
        //render this item
        $out = ''; #render here
        if (!$this->isActive()) { 
            return $out;
        }
        $out .= '<ul>';
        foreach ($this->children as $child) {
            $out .= (string) $child;
        }
        $out .= '</ul>';
        return $out;
    }

    public function isActive() {
        if ($this->active) {
            return true;
        }
        foreach ($this->children as $child) {
            if ($child->isActive()) {
                return true;
            }
        }
        return false;
    }
 }

Then, all you have is a collection of root menu items in an array... To build your menu, you just do:

$rootItems = array($item1, $item2);
$out = '<ul>';
foreach ($rootItems as $item) {
    $out .= (string) $item;
}
$out .= '</ul>';

I'll leave the semantics of constructing the object, adding children, etc to the user...

ircmaxell
+1  A: 

What about rewrite nav function in the next way:

function nav($links, $level){       
    foreach($links as $k => $v) {
        if (is_array($v)) {
            nav($v, $level + 1)
        } else {
            echo buildLinks($k, $v, $base);
        }
    }
}

And than call it:

$links = array(
array(
"Home" => "home.php",
"submenu1" => array(
    "something1"=>"something1.php",
    "something2" => "something2.php")
),
array("Pics" => "pics.php"),
array("About" => "about.php")
);
nav($links, 0);
Zyava
+1  A: 

Simplest way, IMHO, is to just make a recursive call, and use a tree structured description of your navigation (that is, nested arrays). Untested example code:

<?php
$links = array(
    "Home" => array("home.php", array(
        "something1"=> array("something1.php", array()),
        "hello"=> array("hello.php", array(
            "world" => array("world.php", array()),
            "bar" => array("bar.php", array()),
        )),
    )),
    "Pics" => array("pics.php", array(
        "people"=>"people.php",
        "places" => "places.php",
    )),
    "About" => array("about.php", array()), // example no subitems
);

// use the following $path variable to indicate the current navigational position
$path = array(); // expand nothing
$path = array('Home'); // expand Home
$path = array('Home', 'hello'); // also expand hello in Home

// map indent levels to classes
$classes = array(
    'item',
    'subitem',
    'subsubitem',
);


// recursive function to build navigation list
function buildNav($links, $path, $classes)
{
    // selected page at current level
    // NOTE: array_shift returns NULL if $path is empty.
    // it also alters the array itself
    $selected = array_shift($path);
    $class = array_shift($classes);

    echo "<ul>\n";

    foreach($links as $name => $link)
    {
        list($href, $sublinks) = $link;
        if ($name == $selected)
        {
            echo "<li class=\"selected $class\"><a href=\"$href\">$name</a>\n";
            // recursively show subitems
            // NOTE: path starts now with the selected subitem
            buildNav($sublinks, $path, $classes);
            echo "</li>\n";
        }
        else
        {
            echo "<li><a href=\"$href\" class=\"$class\">$name</a></li>\n";
        }
    }

    echo "<ul>\n";
}

// actually build the navigation
buildNav($links, $path, $classes);
?>
catchmeifyoutry
+3  A: 

Don't reinvent the wheel, use Zend_Navigation and you will be happy.

takeshin
Using it outside the realm of the entire framework seems less than easy. Trickier than using other components like Mail or Log, anyway.
rg88
Now that I've looked at it in depth it does not seem possible to integrate Zend_Navigation into a site without using the all of the MVC components of that framework. Correct me if I'm wrong.
rg88
If you dont't have to use zend models and controllers. You don't have to even use the view, but in this case you need to do some tricks to easily use Zend_View_Helper_Navigation, which is an important part of Zend_Navigation.
takeshin
+1  A: 

You were on the right track with RecursiveIteratorIterator. It essentially flattens a recursive iterator. Here is the correct way:

$nav = array(
    array(
    "Home" => "home.php",
    "submenu1" => array(
        "something1"=>"something1.php",
        "something2" => "something2.php")
    ),
    array("Pics" => "pics.php"),
    array("About" => "about.php"),
);

$it = new RecursiveIteratorIterator(
    new RecursiveArrayIterator($nav),
    RecursiveIteratorIterator::SELF_FIRST
);

foreach ($it as $k => $v) {
    if ($it->getDepth() == 0)
        continue;
    echo str_repeat("    ", $it->getDepth() - 1) .
        "$k => $v\n";
}

gives

Home => home.php
submenu1 => Array
    something1 => something1.php
    something2 => something2.php
Pics => pics.php
About => about.php
Artefacto
+1  A: 

I think you should read this post!

hope this help!

aSeptik