Tree Helper
A helper to generate nested ULs OLs, DIVs or whatever from tree data.
Works best with the TreeBehavior, but not a requirement.
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
Bug
1 Incorrect variable name line 91
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 !
Comment
2 Corrected
Thanks for pointing that out - now corrected in the article.
Question
3 Remove empty UL elements
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.
Question
4 urls
Comment
5 re urls
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
Question
6 first child and last child
Comment
7 Found a solution
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;
}
Question
8 Possible Enhancement and Question
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?
Bug
9 A small typo
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))) {