User Tutorial: Creating a CSS Menu

A relatively common question I’ve seen on the google group is how to create a CSS Menu; some time ago I pasted a basic principle which could be used to define a menu which was either dynamic or static, and thought I’d try to elaborate upon the original paste. If anyone feels they can add to (KISS!) or correct any elements of this tutorial, please do.

Introduction

To start at the beginning...

What is a CSS menu?

A CSS menu is typically made up of a simple unordered list of links, formatted with css to look like any style of menu you choose to define. If you would like to see an example, here’s a simple one to get going: http://css.maxdesign.com.au/listamatic/vertical10.htm

In this tutorial I am not going to go into how to style the CSS for your menu, the above web page contains several examples if you are looking for inspiration.

What's the advantage of using a CSS menu?

If you wish to change the way your menu items look, where they are placed, etc. you need only modify the CSS. It also makes reviewing the source code an easier task.

Alternatives to this approach

A different approach to creating a menu, is presented in the tutorial for MenuBaker Elemenent.

The Simple Menu

If you have a simple menu that never changes and you just want it to be present on all pages, in the same place, you may find that defining the HTML for your menu is the easiest solution. however, as a stepping stone to more complex examples, let’s consider how to solve this problem using a semi-scalable solution.

Let’s start as we mean to go on, the first thing we will do is create a helper which we will use to convert an array of menu options into an unordered list. As this function is HTML related, we’ll put it in our own html helper.

So, create or edit this file: /app/views/helpers/my_html.php

<?php
require_once(LIBS . DS . 'view' . DS . 'helpers' . DS . 'html.php');
 
class MyHtmlHelper extends HtmlHelper
{
/**
 * Recieves an array of menu items, each item consisting of 2 or 3 elements: 
 * Name for link; URL; Sub menu items (optional).
 * 
 * The output of this method is a div containing an unordered list which reflects 
 * the same structure as the input array. 
 * The second parameter is used to define the id of the output div.
 *
 * @param array $listItems
 * @param string $here
 * @param string $id
 * @return string Unordered html list.
 */
    function arrayToUl($listItems,$here="Who Knows",$id="menu")
    {
        $return = "<div id=\"$id\">";
        foreach ($listItems as $Item)
        {
            $return .= $this->_printMenu ($Item, $here);
        }
        $return .= "</div>";
        return $return;
    }
 
/**
 * Internal method, called by arrayToUl. 
 * Allows the possibility to have unlimited levels of UL.
 *
 * @param array $menu
 * @param int $depth
 * @return string Unordered html list.
 */
    function _printMenu ($Menu,$here,$depth = 1)
    {
        if ($depth == 1)
      	{
            $return = "<ul><li><h2>";
        }
        else
        {
            $return = "<li>";          
        }
 
        $controllerName = $this->_getControllerName($Menu[0]);
        if (strtolower($here) == strtolower($controllerName))
        {
             $Options = Array('class'=>'active');
        }
         else 
        {
             $Options = NULL;  
        }
 
        if ($Menu[1])
        {
            $return .= $this->link($Menu[0], $Menu[1], $Options)."</li>";      
        }
        else
        {
             $return .= $Menu[0]."</li>";
        }
 
        if ($depth == 1)
        {
            $return .= "</h2>";
        }
        else
        {
            $return .= "";          
        }
 
        if (isset($Menu[2]))
        {
            $return .= "<ul>";
            foreach ($Menu[2] as $SubMenuItem)
            {
                $return .= $this->_printMenu ($SubMenuItem, $here,$depth+1);
            }
            $return .= "</ul>";
        }
 
        if ($depth == 1)
        {
            $return .= "</li></ul>";
        }
        else
        {
            $return .= "</li>";          
        }
        return $return;
    }
 
/**
 * Internal method, called by _printMenu. 
 * Determins the controller for a given URL.
 *
 * @param array $MenuItem
 * @param int $depth
 * @return string Unordered html list.
 */
    function _getControllerName ($MenuItem)
    {
        if ($MenuItem)
        {
            // Remove # from url if it's present
            $pos1 = strpos($MenuItem, "#");
            if ($pos1 !== false)
            {
                $MenuItem = substr($MenuItem, 0, $pos1);
            }
     
            $URLArray = explode("/", $MenuItem);
            if (isset($URLArray[1]))
            {
                if ($URLArray[1]!="")
                {
                    return $URLArray[1];
                }
                else
                {
                    return 'pages';
                }
            }
            else
            {
                return 'pages';
            }
        }
    }
}
?>

This helper needs to be declared to be available for use, so let’s do that now:

create or edit this file: /app/app_controller.php

class AppController extends Controller {
 
var $helpers = array(
			// Include the My Html helper, along with any other helpers that are required.
			//'Html', Not needed, included with require statement in myHtml Helper
			'MyHtml',
			//'Ajax',
			//'Javascript'
		);
// rest of code
}

Up until now, all we’ve done is make the tools available to be used. Now that they are available, it’s time to create an element to make use of these tools and produce a menu

Create this file /app/views/elements/menu.thtml

<?php 
    $MenuItems = array (
    	array ('home','/') ,
    	array ('one','/one/',
            array (
                   array('1one','/one/#one'),
                   array('1two','/one/#two'),
                   array('1three','/one/#three')
                  )
        ),
    	array ('two','/two/',
            array (
                   array('2one','/two/sub/'),
                   array('2two','/two/sub2/')
                   )
        ),
    	array ('Just a title',NULL,
            array (
                   array('3one','/some/where/'),
                   array('3two','/some/where/else'),
                   array('3three','/some/where/outthere')
                   )
        ),
    	array ('last','/last/')
    );
echo $myHtml->arrayToUl ($MenuItems, $this->name);	  
?>

And finally, include the element in your layout, so that it appears on all pages. Assuming you are using the default layout:

create or edit this file /app/views/layouts/default.thtml

<?php 
// Add this line where you want you menu to appear
echo $this->renderElement('menu',$params);
?>

What have we achieved?

All being well, you now have an unordered list which looks something like this:

  • Home
  • One
  • * 1one
  • * 1two
  • * 1three
  • Two
  • * 2one
  • * 2two
  • Three
  • * 3one
  • * 3two
  • * 3three

With a little CSS (see http://css.maxdesign.com.au/listamatic/vertical10.htm for one of many examples) this can be easily transformed into a styled menu. If you’ve made it this far, hopefully you are looking for a more complex example...

The Dynamic Menu

If you need the possibility to change anything about the menu items (such as adding menu items when someone logs in) you can modify the simple menu to do this for you. Instead of defining the variable MenuItems in the menu element, we’ll define it in a component, which will again be called by all pages.

Assuming that you already have followed the steps for the simple menu, on to some set up steps - include the menu component.

edit /app/app_controller.php

class AppController extends Controller {
 
    var $components = array('Menu');
 
    var $helpers = array(
			// Include the My Html helper, along with any other helpers that are required.
			//'Html', Not needed, included with require statement in myHtml Helper
			'MyHtml',
			//'Ajax',
			//'Javascript'
 
    
    		);
// rest of code
}

and create this file /app/controllers/components/menu.php

<?php
class MenuComponent extends Object {
    var $controller = true;
    var $MenuItems = array 
    (
        array ('home','/') ,
        array ('one','/one/',
            array (
                   array('1one','/one/#one'),
                   array('1two','/one/#two'),
                   array('1three','/one/#three')
                  )
        ),
        array ('two','/two/',
            array (
                   array('2one','/two/sub/'),
                   array('2two','/two/sub2/')
                   )
        ),
        array ('Just a title',NULL,
            array (
                   array('3one','/some/where/'),
                   array('3two','/some/where/else'),
                   array('3three','/some/where/outthere')
                   )
        ),
        array ('No link, no children',NULL,
            array (
                   )
        ),
        array ('last','/last/'),
    );
 
 
/**
 * Startup method
 * Called automatically, used to set the variable to define the menu.
 * Could equally be used to define a context menu, based upon the controller name and action.
 *
 * @return null
 */
    function startup(&$controller)
    {
        $this->controller = &$controller;
        $this->MenuItems = $this->checkGroup($this->MenuItems);
        $this->controller->set('MenuItems',$this->MenuItems);
    }
    
/**
 * Check groups
 * Perform any logic desired to delete groups.
 * In this code, if a group is defined which is not a link and has no children it is removed 
 *
 * @param array $menu
 * @return array
 */
    function checkGroup ($Menu) 
    {
        if ($Menu)
        {
            if (!is_string($Menu[0])) /* Then this is the top level */
            {
                foreach ($Menu as $key=>$Array)
                {
                    $result = $this->checkGroup ($Array);
                    if ($result)
                    {
                      $Menu[$key] = $result;
                    }
                    else
                    {
                      unset($Menu[$key]);
                    }
                }
                if (count($Menu)==0)
                {
                    unset($Menu);
                    return false;
                }
            }
            else
            {
                if ($this->checkItem($Menu[1]))
                {
                    if(isset($Menu[2]))
                    {
                        $Menu[2] = $this->checkGroup ($Menu[2]);
                        $hasChildren = $Menu[2]?true:false;
                    }
                    else
                    {
                        $hasChildren = false;
                    }
                }
                else
                {
                    return false;
                }
     
                $hasLink = $Menu[1]?true:false;
                if ((!$hasLink)&&(!$hasChildren))
                {
                    return false;
                }
            }                  
            return $Menu;
        }
        else
        {
          return false;
        }
    }
 
/**
 * Check items
 * Perform any logic desired to delete an item.
 * In this code, no test is performed, but could perform an ACL check for example to remove links to pages
 * which are not permited.
 *
 * @param string $MenuItem
 * @return true or false
 */
    function checkItem ($MenuItem)
    {
        return true;
    }
}
?>

and edit the menu element that we previously created

<?php 
// Don't need to define any extra variables, the menu was defined in the controller->component.
echo $myHtml->arrayToUl ($MenuItems, $this->name);	  
?>

You should now have the same results as for the static menu example, but should any logic be added, it’s now in the right place. There is a menu group defined “No link, no children” which is there purely to demonstrate a simple test. This menu item is not a link, and also has no children - in this case it might be the desired behaviour that this parent is hidden (in this example, it is coded to have no children, a more realistic example would be that it has children with the user isn’t permitted to view and are pruned by the checkItem method), which is what the example does.

More than one menu?

You want to have more than one menu? No problem. All that you would need to do is set 2 variables in the component init method, and put calls to arrayToUl where you want your menu items to appear. e.g.

<?php 
// Need to define $MainMenu in the menu component
echo $myHtml->arrayToUl ($MainMenu, $this->name,"Main");	  
// Need to define $ContextMenu in the menu component
echo $myHtml->arrayToUl ($ContextMenu, $this->name,"Context");	  
?>

Conclusion 4 now

With the approach outlined here it should be possible to define your menu in any way you want. If, for example, you want to generate the menu items from a database or based on the time of day or any other crazy user defined logic - you can do that from within the component.

 
tutorials/css_menus.txt · Last modified: 2006/04/08 13:33 by ad7six