Tree Helper

By Andy Dawson aka "AD7six"
A helper to generate nested ULs OLs, DIVs or whatever from tree data.

Works best with the TreeBehavior, but not a requirement.

Example usage 1:


Download code <?php
//controller code:
$stuff $this->MyTreeBehaviorModel->find('all'
     array(
'fields' => array('whatever''lft''rght'), 'order' => 'lft ASC'));
$this->set('stuff'$stuff);
//view code
echo $tree->generate($stuff);

Example usage 2:


Download code <?php
//controller code:
$stuff $this->NoTreeBehaviorModel->findAllThreaded();
$this->set('stuff'$stuff);
//view code
echo $tree->generate($stuff);

Example usage 3:


Download code <?php
//controller code:
$id 1;
$showMeChildren true;
$stuff $this->MyTreeBehaviorModel->children($id$showMeChildren);
$this->set('stuff'$stuff);
//view code
echo $tree->generate($stuff);

Example usage 4:


Download code <?php
//controller code:
$stuff $this->MainModel->MyTreeBehaviorModel->find('all'
     array(
'fields' => array('whatever''lft''rght'), 'order' => 'lft ASC'));
$this->set('stuff'$stuff);
//view code
echo $tree->generate($stuff, array('Model' => 'MyTreeBehaviorModel'));

Example usage 5:


Download code <?php
//controller code:
$stuff $this->MyTreeBehaviorModel->find('all'
     array(
'fields' => array('whatever''lft''rght'), 'order' => 'lft ASC'));
$this->set('stuff'$stuff);
//view code
echo $tree->generate($stuff, array('type' => 'ol')); // generate an ol not a ul

Example usage 6:


Download code <?php
//controller code:
$stuff $this->MyTreeBehaviorModel->find('all'
     array(
'fields' => array('whatever''lft''rght'), 'order' => 'lft ASC'));
//view code
// pass the data for each node to /views/elements/thisone.ctp and put the results in the rendered UL
echo $tree->generate($stuff, array('element' => 'thisone'));


Helper Class:

Download code <?php 
/**
 * Tree Helper.
 *
 * Used the generate nested representations of hierarchial data
 *
 * PHP versions 4 and 5
 *
 * Copyright (c), Andy Dawson
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @copyright    Copyright (c) 2007, Andy Dawson
 * @version      $Revision: 20 $
 * @created      24/01/2008
 * @modifiedby   $LastChangedBy: andy $
 * @lastmodified $Date: 2008-03-11 20:38:20 +0100 (Tue, 11 Mar 2008) $
 * @license      http://www.opensource.org/licenses/mit-license.php The MIT License
 */

/**
 * Tree helper
 *
 * Helper to generate tree representations of MPTT or recursively nested data
 */
class TreeHelper extends AppHelper {

    var 
$helpers = array ('Html');

/**
 * Tree generation method.
 *
 * Accepts the results of 
 *     find('all', array('fields' => array('lft', 'rght', 'whatever'), 'order' => 'lft ASC'));
 *     children(); // if you have the tree behavior of course!
 * or     findAllThreaded(); and generates a tree structure of the data.
 *
 * Settings (2nd parameter):
 *    'model' => name of the model (key) to look for in the data array. defaults to the first model for the current
 * controller.
 *    'alias' => the array key to output for a simple ul (not used if element is specified)
 *    'type' => type of output defaults to ul
 *    'itemType => type of item output default to li
 *    'class' => class for top level 'item'
 *    'element' => path to an element to render to get node contents.
 *
 * @param array $data data to loop on
 * @param array $settings
 * @return string html representation of the passed data
 * @access public
 */
    
function generate ($data$settings = array ()) {
        
$element false;
        
$class false;
        
$model null;
        
$options '';
        
$alias 'name';
        
$left 'lft';
        
$right 'rght';
        
$type 'ul';
        
$itemType 'li';
        
$depth 0;
        
extract($settings);

        
$view =& ClassRegistry:: getObject('view');
        if (
$model === null) {
            
$model Inflector::classify($view->params['models'][0]);
        }
        
$stack = array();
        if (
$class) {
            
$options .= ' class="' $class '" ';
        }
        
$return "\r\n" '<' $type $options'>';
        foreach (
$data as $i => $result) {
            
// Prefix
            
while ($stack && ($stack[count($stack)-1] < $result[$model][$right])) {
                
array_pop($stack);
                
$return .= "\r\n" str_repeat("\t",count($stack) + 1) . '</' $type '>';
                
$return .= '</' $itemType '>';
            }
            
$return .= "\r\n" str_repeat("\t",count($stack) + 1) . '<' $itemType '>';

            if (!
$model) {
                
$result[$model] = $result
            }

            
// Some useful vars
            
$hasChildren $firstChild $lastChild $hasVisibleChildren false;
            
$numberOfDirectChildren $numberOfTotalChildren 0;
            if (isset(
$result['children'])) {
                if (
$result['children']) {
                    
$hasChildren $hasVisibleChildren true;
                    
$numberOfDirectChildren count($result['children']);
                }
                if (!isset(
$data[$i 1])) {
                    
$firstChild true;
                }
                if (!isset(
$data[$i 1])) {
                    
$lastChild true;
                }
            } elseif (isset(
$result[$model][$left])) {
                if (
$result[$model][$left] != ($result[$model][$right] - 1)) {
                    
$hasChildren true;
                    
$numberOfTotalChildren = ($result[$model][$right] - $result[$model][$left] - 1) / 2;
                    if (isset(
$data[$i 1]) && $data[$i 1][$model][$right] < $result[$model][$right]) {
                        
$hasVisibleChildren true;
                    }
                }
                if (!isset(
$data[$i 1]) || ($data[$i 1][$model][$left] == ($result[$model][$lft] - 1))) {
                    
$firstChild true;
                }
                if (!isset(
$data[$i 1]) || ($stack && $stack[count($stack) - 1] == ($result[$model][$right] + 1))) {
                    
$lastChild true;
                }
            }
    
            
// Main Content
            
if ($element) {
                
$elementData = array(
                    
'data' => $result
                    
'depth' => $depth?$depth:count($stack) + 1,
                    
'hasChildren' => $hasChildren,
                    
'numberOfDirectChildren' => $numberOfDirectChildren,
                    
'numberOfTotalChildren' => $numberOfTotalChildren,
                    
'firstChild' => $firstChild,
                    
'lastChild' => $lastChild,
                    
'hasVisibleChildren' => $hasVisibleChildren
                
);
                
$return .= $view->renderElement($element,$elementData);
            } else {
                
$return .= $result[$model][$alias];
            }

            
// Suffix
            
if (!isset($result[$model][$right]) || !isset($result[$model][$left]) || isset($result['children'])) {
                if (isset(
$result['children'])) {
                    unset(
$settings['class']);
                    
$settings['depth'] = $depth 1;
                    
$return .= $this->generate($result['children'], $settings);
                }
                
$return .= '</' $itemType '>';
            } elseif (
$result[$model][$right] == $result[$model][$left] + 1) { // Has no children
                
$return .= '</' $itemType '>';
            } else {
                
$return .= '<' $type '>';
                
$stack[] = $result[$model][$right];
            }
        }
        while (
$stack) {
            
array_pop($stack);
            
$return .= "\r\n" str_repeat("\t",count($stack) + 1) . '</' $type '>';
            
$return .= '</' $itemType '>';
        }
        
$return .= "\r\n" '</' $type '>' "\r\n";
        return 
$return;
    }
}
?>


Variables available in your element


If you choose to nominate an element to be used to render the contents of each node, the following variables are automatically available inside the element:
Download code
<?php
$data 
// the row of data passed to the helper
$depth // depth in the current tree 1 = first item
$hasChildren // whether the current row has children or not
$hasVisibleChildren // whether the current row has Visible children or not. Only relavent for MPTT tree data
$numberOfDirectChildren // only avaliable with recursive data
$numberOfTotalChildren // only available with MPTT tree data
$firstChild // whether the current row is the first of it's siblings or not
$lastChild // whether the current row is the last of it's siblings or not

Comments 609

CakePHP team comments Author comments

Bug

1 Incorrect variable name line 91

Fantastic work ! By far the most reliable technique to display a mptt tree. A little correction though :

Line 91 :

Controller Class:

<?php 
$numberOfChildren 
$numberOfTotalChildren 0;
?>


Should be :

Controller Class:

<?php 
$numberOfDirectChildren 
$numberOfTotalChildren 0;
?>


Also, don't forget to pass the name of the model if you use this helper globally (like a navigation menu in the default layout). I failed to remember that and made my Apache crash !
posted Wed, Mar 12th 2008, 07:07 by Pierre Emmanuel Fringant

Comment

2 Corrected

Hi Pierre,

Thanks for pointing that out - now corrected in the article.
posted Wed, Mar 12th 2008, 07:54 by Andy Dawson

Question

3 Remove empty UL elements

This is a great helper. However, i was wondering if it is possible to remove the empty UL elements that are added after each node without a child.

eg)

View Template:

<ul id="nestList" >
    <li><a href="somelink">Windows</A>
<ul>
    <li><a href="someOtherLink">File System</A>
<ul>
</ul>
</li>
</ul>
</li>
</ul>


I'm very new to Cake and not a very experienced programmer, having looked at the helper code i cannot see how to remove the empty UL's. I ask as i have some collapsible menu javascript code which does not work properly with the empty UL elements after all childless nodes. Im currently passing in data using findAllThreaded(), Any help would be greatly appreciated.
posted Thu, Apr 3rd 2008, 05:16 by Simon Wass

Question

4 urls

I have this working but i dont understand how give to nodes the url to go :?
posted Fri, Apr 4th 2008, 02:07 by minskog

Comment

5 re urls

I have this working but i dont understand how give to nodes the url to go :?

You need to create an element and place it in views/elements.
your tree generation in the view should look similar to this.

View Template:

echo $tree->generate($threadedlist, array('element'=>'elementName', 'model'=>'Category'));

Then in the element, you simply echo whatever you would like to appear in the node, the $tree-generate makes an array variable called '$data' available to the element which the node element it is about to print to the screen. Check the helper code above, there is also a lot of other useful information in the $data array which can be used to make your output conditional or to tailor it to your needs. So my element looks similar to this

View Template:


$return = '<A HREF="/categories/view_cat/'.$data['Category']['id'].'">';
$return .= $data['Category'][$lang.'_description']."</A>";


I think you can use the $html->link method in the element also but i chose this way. remember, the element is called for every node in the list so you only need to echo the url or whatever information for that single line in the list.

Hope this helps
posted Thu, Apr 10th 2008, 00:10 by Simon Wass

Question

6 first child and last child

Is there a way to know if current rendered element is first child or last child within it's depth? Currently it seems it's only possible to know if it's the very first and very last child of the tree?
posted Mon, Apr 21st 2008, 17:01 by Kim Biesbjerg

Comment

7 Found a solution

I added an extra check in each of the if-statements setting $firstChild and $lastChild to true.

I added this to add firstChild and lastChild classes to html elements for all depths/levels allowing for greater CSS styling flexibility.



if (!isset($data[$i - 1]) || ($data[$i - 1][$model][$left] == ($result[$model][$right] - 1)) || $data[$i - 1][$model][$left] + 1 == $result[$model][$left]) {
    $firstChild = true;
}
if ($stack && $stack[count($stack) - 1] == ($result[$model][$right] + 1) || !isset($data[$i + 1])) {
    $lastChild = true;
}

posted Wed, Apr 30th 2008, 04:22 by Kim Biesbjerg

Question

8 Possible Enhancement and Question

Awesome helper! Thanks for sharing it. Has made outputting tree data a whole lot easier.

An enhancement (that I don't have the skills to implement myself and I'm sure could be added easily by those with skills):

1) User selectable class name for each li ... maybe one of the vars in $data. User provides $liclass = 'Model.field' as a setting. Can be usefull for styling/css control purposes.

Also a question:

I might be reading the code wrong but I can't find out how to 'turn off' the output of the ul/ol and li's ie to control the output manually using the element setting. Any hints?
posted Mon, May 12th 2008, 21:23 by Scott

Bug

9 A small typo

Nice piece of code Andy.
Am I wrong or there is a typo (a $lft instead of $left) ?

if (!isset($data[$i - 1]) || ($data[$i - 1][$model][$left] == ($result[$model][$lft] - 1))) {
posted Wed, May 14th 2008, 09:14 by Luigi Gualtieri

Login to Submit a Comment