Categories > TinyButStrong general >

Tree structure

The forum is closed. Please use Stack Overflow for submitting new questions. Use tags: tinybutstrong , opentbs
By: RwD
Date: 2005-02-24
Time: 15:59

Tree structure

I am trying to build up a tree from a database structure but for one reason or the othe I cannot see how to do it for an unknown number of subtrees

First the db structure of the tree.
Basically it is a celko nested set but I maintain the parent-child structure for security.
(http://www.sitepoint.com/article/hierarchical-data-database/2 to know more about the nested set structure)

This is the tree in the database (I made a tree in the name column for easy reading)
node_name       node_id  parent_id  Left  Right  Level
Fruit           1        0          1     16     0
+ Red           2        1          2     9      1
| + Cherry      4        2          3     4      2
| + Strawberry  8        2          5     6      2
| + Red apple   7        2          7     8      2
+ Yellow        3        1          10    15     1
  + Lemon       5        3          11    12     2
  + Banana      6        3          13    14     2

Now I would like to display this tree as follows:
<ul>
  <li>Fruit
    <ul>
      <li>Red
        <ul>
          <li>Cherry</li>
          <li>Strawberry</li>
          <li>Red apple</li>
        </ul>
      </li>
      <li>Yellow
        <ul>
          <li>Lemon</li>
          <li>Elstar</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

As you can see it sounds all very simple but for some reason or the other tbs does not want me to make lists as deep as the data happens to indicate instead I seem to have to make up a depth and hope the actual data does not exceed this...
Does anybody know of a generic way to get the specified output using tbs and the shown data structure?

I myself have been trying along the way of a template including itself:
($tmpl['menu'] = 'menu.tpl';)
<ul>
    <li>[merge menuname here]
    [var.tmpl.menu;file=[val]]
    </li>
</ul>
But all my attempts were completely unsuccessfull :(
By: RwD
Date: 2005-02-24
Time: 16:44

Re: Tree structure

To make it easier I wrote a script that runs on a resultset being retrieved in the order I posted before. It then only uses the name and level elements (retrieval was one query sorted on the "left" value where "left > startnode.left" and "right < startnode.right" (If you don't understand then read the link I posted before)).

function fnGetMenuTree
( $celko
, $parent_id )
{
    //
    // Get the structure & assign the resultset
    $celko->SelectSubNodes( $parent_id );
    $Resultset     = $celko->DB;

    //
    // Get all the nodes
    if ( $Resultset->has_rows() ) {
        $result         = '';
        $last_level     = -1;
       
        //
        // Loop over rows
        while ( $Resultset->next_record() ) {
            if ( $Resultset->f('menu_level') > $last_level )
                $result    .= '<ul>';
            elseif ( $Resultset->f('menu_level') < $last_level )
                for ( $i = 0 ; $i <= $Resultset->f('menu_level') ; $i++ )
                    $result    .= '</li></ul>';
            else
                $result .= '</li>';

            $result        .= '<li>'.$Resultset->f('menu_level') .'-'. $Resultset->f('menu_name');
            $last_level     = $Resultset->f('menu_level');
        }

        for ( $i = 0 ; $i <= $last_level ; $i++ )
            $result    .= '</li></ul>';

        $Resultset->free();
        $celko->DB->free();
    }

    return $result;
}
By: Skrol29
Date: 2005-02-24
Time: 16:48

Re: Tree structure

Hi RwD,

This is not obvious because automatic TBS special blocks are only headergrp, footergrp and splitgrp which are activated when a value changes. But in the cas of hierachical representation, value changing is not enougth, it matters also if it's growing or decreasing.

If it is ok for you to close <li> tags just after the item, instead of after the children items, the I can propose a solition.
Here is the target result :
<ul>
  <li>Fruit</li>
    <ul>
      <li>Red </li>
        <ul>
          <li>Cherry</li>
          <li>Strawberry</li>
          <li>Red apple</li>
        </ul>
      <li>Yellow</li>
        <ul>
          <li>Lemon</li>
          <li>Elstar</li>
        </ul>
    </ul>
</ul>

Then you can code:
[blk;block=begin;onformat=m_blk_onformat]
  [blk.u_open;htmlconv=no]
    <li>[blk.node_name]</li>
  [blk.u_close;htmlconv=no]
[blk;block=end]

Html:
$PrevLevel = false;
$TBS->MergeBlock('blk','SELECT node_name, Level FROM MyTable ORDER BY Left');
...
function m_blk_onformat($Block,&$CurrRec,&$Src,$RecNum) {
  global $PrevLevel;
   if ($PrevLevel===false) $PrevLevel = $CurrRec['Level'] -1;
   $CurrRec['u_open'] = str_repeat('<u>',max(0,$CurrRec['Level']-$PrevLevel));
   $CurrRec['u_close'] = str_repeat('</u>',max(0,$PrevLevel-$CurrRec['Level']));
   $PrevLevel = $CurrRec['Level']; 
}
By: RwD
Date: 2005-02-24
Time: 16:52

Re: Tree structure

No, it is not acceptable to have an ul inside another ul. This is not valid HTML4.01 strict which I am affraid I have to code. The ul element can appear within the li element, so that's where it needs to be.

Perhaps this is something to build into a later version of tbs? Because if tbs cannot do it I will have to go with the function I posted here :(

bummer :P
By: Skrol29
Date: 2005-02-24
Time: 17:21

Re: Tree structure

Ok, that's true.

So all becomes more complicated, but here is the solution.
HTML:
[blk;block=begin;onformat=m_blk_onformat]
  [blk.open;htmlconv=no]
    [blk.node_name]
  [blk.close;htmlconv=no]
[blk;block=end]
[var.ExtraRec.close;htmlconv=no]

PHP:
// Merging data
$PrevLevel = false;
$TBS->MergeBlock('blk','SELECT node_name, Level FROM MyTable ORDER BY Left');

// Now we need to close all level from the last record
$x = '';
$ExtraRec = array('Level'=>0);
m_blk_onformat('',$ExtraRec,$x,0);

...
function m_blk_onformat($Block,&$CurrRec,&$Src,$RecNum) {

  global $PrevLevel;
  if ($PrevLevel===false) $PrevLevel = $CurrRec['Level'] -1;

  $delta = $CurrRec['Level']-$PrevLevel);
  if ($delta==0) {
    $CurrRec['open'] = '<li>';
    $CurrRec['close'] = '</li>';
  } else {       
    $CurrRec['open'] = str_repeat('<li><u>',max(0,$delta));
    $CurrRec['close'] = str_repeat('</u></li>',max(0,-$delta));
  }

  $PrevLevel = $CurrRec['Level'];
}
By: RwD
Date: 2005-02-24
Time: 20:28

Re: Tree structure

Is there perhaps a local/global problem?
I get this error after removing the parsing errors I got

TinyButStrong Error (Array value): Can't merge [blk.open] because there is no key named 'open'. This message can be cancelled using parameter 'noerr'.

TinyButStrong Error (Array value): Can't merge [blk.close] because there is no key named 'close'. This message can be cancelled using parameter 'noerr'.

For as far as I can see the CurrRec variable is a refference to the actual variable so it should be allowed to be changed. Got me puzzeld
By: RwD
Date: 2005-02-24
Time: 20:31

Re: Tree structure

Update :P

I put echo "hi"; in the onformat function and it only gets called by the ExtraRec part :S
By: RwD
Date: 2005-02-24
Time: 20:32

Re: Tree structure

Update 2 :P

I forgot to mention that it does output the node names all below eachother
By: RwD
Date: 2005-02-24
Time: 21:47

Re: Tree structure

I was looking at the onformat function, and it isn't as it should be. So looking it up I think onformat should have been onsection ;)

In any case, it is functioning but not working...
This is the output at home (different structure) (I added the level - to the name):
  <li><ul>
    0 - Root
 

  <li><ul>
    1 - SubA
 

  <li><ul>
    2 - SubA1
 

  <li>
    2 - SubA2
  </li>

 
    1 - SubB
  </ul></li>

</ul></li>

It should have been
<ul>
  <li>0 - Root
    <ul>
      <li>1 - SubA
        <ul>
          <li>2 - A1</li>
          <li>2 - A2</li>
        </ul>
        </li>
      <li>1 - SubB</li>
    </ul>
  </li>
</ul>

If you know the solution you are welcome to tell me. But now I have a functioning piece of code I will probably make it.

But I am wondering what the big advantage of this system is over the function I posted above. the html is in the php script!!!!
By: Pirjo Posio
Date: 2005-02-24
Time: 22:09

Re: Tree structure

Can't say if it's best to use your own php-script or tbs.
I was just wondering whether it would be ok to use only <div> and no <ul><li>-pairs, like this (part of php-file, not tbs)

'<div style="margin-left: ' . $item['level'] . 'em;">' . $item['node_name'] . '</div>'

Maybe this is easier to modify into a tbs-template?
By: RwD
Date: 2005-02-24
Time: 22:18

Re: Tree structure

Thanks for the suggestion.

It might be easier, however, I am building websites using meaningfull tags and no inline styles. so menu's I only build with lists or definition lists (dl,dt and dd)

I did not discuss any styling but you offer styling as the solution :(
By: RwD
Date: 2005-02-24
Time: 22:55

Re: Tree structure

Sorry for the message spree :P

But I solved the puzzle by just copying my own code into the function. the following was changed:
  if ( $delta > 0 ) {
    $CurrRec['open'] = '<ul><li>';
    $CurrRec['close'] = '';
  } elseif ( $delta < 0 ) {
    $CurrRec['open'] = '</li>'.str_repeat('</ul></li>',max(0, -$delta)).'<li>';
    $CurrRec['close'] = '</li></ul>';
  } else {
    $CurrRec['open'] = '</li><li>';
    $CurrRec['close'] = '';
  }

But still I wonder how this way is any better from my function. Do you think we just have to manage with this way, or will a structural solution be added in the future :o)
(I have no idea if it is easy to create; this does imply recursion I think (unlike the solution we both came to now))

Anyway, thanks for the help :)
By: RwD
Date: 2005-02-25
Time: 09:45

Re: Tree structure

Trying to be complete I will now post the solution as it is working now. It is different from the code skrol29 posted and some variable names should be changed I think. But for future reference this will do:

PHP:
function m_blk_onsection
( $Block
, &$CurrRec
, &$Src
, $RecNum )
{
    global $FirstLevel, $PrevLevel;

    if ( $PrevLevel === false ) {
        $FirstLevel     = $CurrRec['Level'] - 1;
        $PrevLevel     = $CurrRec['Level'] - 1;
    }

    $delta     = $CurrRec['Level'] - $PrevLevel;

    if ( $delta > 0 ) {
        $CurrRec['open']     = "<ul><li>";
        $CurrRec['close']     = "";
    } elseif ( $delta < 0 ) {
        $CurrRec['open']     = "</li>" . str_repeat('</ul></li>',max(0, -$delta)) . "<li>";
        $CurrRec['close']     = str_repeat('</li></ul>',max(0, -$delta));
    } else {
        $CurrRec['open']     = "</li><li>";
        $CurrRec['close']     = "";
    }
    $PrevLevel     = $CurrRec['Level'];
}


//
// Merging data
$PrevLevel     = false;
$menu_tree_root     = 1;
$tbs->MergeBlock( 'blk'
        , $DB->Link_ID
        , 'SELECT node_name, Level FROM MyTable ORDER BY Left' );

//
// Now we need to close all level from the last record
$x         = '';
$ExtraRec     = array('Level'=>$FirstLevel);
m_blk_section('',$ExtraRec,$x,0);

HTML:
[blk;block=begin;onsection=m_blk_onsection]
    [blk.open;htmlconv=no]
    [blk.node_name]
[blk;block=end]
[var.ExtraRec.close;htmlconv=no]

This code will work if your database structure is as I described before. I am using tbs 2.01. I did alter the code before posting it (fieldnames that are different in my db structure, and the query I use is different) but everything should work. If it doesn't I do not suppose it is a lot of work to make it work ;)
By: Youri
Date: 2007-05-11
Time: 18:49

Re: Tree structure

This is great piece of code. Makes perfectly nested ul-li menus.

But what I don't understand is:

When I have new line in open string like this:

  $CurrRec['open']  = "<ul>\n<li>"; 

It outputs new line in final html code. But if I put the same to str_repeat construction:

    $CurrRec['open']  = "</li>" . str_repeat('</ul>\n</li>',max(0, -$delta)) . "<li>";

I get
\n
in output. When I use for loop instead of str_repeat, everything is ok. Seems it has something to do with inner encoding, but I don't know, if it is a matter of TBS or PHP. Any suggestion how to deal with this? I have htmlconvert=no in template, of course.
By: aphexx
Date: 2007-05-15
Time: 12:47

Re: Tree structure

"<ul>\n<li>"

or

'</ul>\n</li>'

see the difference?
single quotes outputs everything as it is written and a newline is not actually \n but a special character so it needs to be parsed.

so in short: replace the single quotes with double quotes and it should work
By: andy
Date: 2007-10-23
Time: 23:25

Re: Tree structure

I used the last code that RwD posted. My table is the same structure as the one RwD posted on the first post, but I got error like this:

TinyButStrong Error in field [blk.open...] : item 'open' is not an existing key in the array. This message can be cancelled using parameter 'noerr'.

TinyButStrong Error in field [blk.open...] : item 'open' is not an existing key in the array. This message can be cancelled using parameter 'noerr'.

TinyButStrong Error in field [blk.open...] : item 'open' is not an existing key in the array. This message can be cancelled using parameter 'noerr'.

TinyButStrong Error in field [blk.open...] : item 'open' is not an existing key in the array. This message can be cancelled using parameter 'noerr'.


Anyone has this problem? Thanks
By: andy
Date: 2007-10-23
Time: 23:54

Re: Tree structure

I tried to test it out, but it didn't work, here's url
http://freeeh.com/tree4/tree.php

also, this is the code:


PHP

<?php

$media_root_path = "../";

include_once($media_root_path . 'libs/tbs_class.php');


//The file cnx_mysql.php contains the following lines :
$cnx_id = mysql_connect('localhost','root','') ;
mysql_select_db('test',$cnx_id) ;

$TBS = new clsTinyButStrong ;
$TBS->LoadTemplate('tree.htm') ;



function m_blk_onsection($Block, &$CurrRec, &$Src, $RecNum )
{
global $FirstLevel, $PrevLevel;

if ( $PrevLevel === false ) {
  $FirstLevel  = $CurrRec['Level'] - 1;
  $PrevLevel  = $CurrRec['Level'] - 1;
}

$delta  = $CurrRec['Level'] - $PrevLevel;

if ( $delta > 0 ) {
  $CurrRec['open']  = "<ul><li>";
  $CurrRec['close']  = "";
} elseif ( $delta < 0 ) {
  $CurrRec['open']  = "</li>" . str_repeat('</ul></li>',max(0, -$delta)) . "<li>";
  $CurrRec['close']  = str_repeat('</li></ul>',max(0, -$delta));
} else {
  $CurrRec['open']  = "</li><li>";
  $CurrRec['close']  = "";
}
$PrevLevel  = $CurrRec['Level'];
}


//
// Merging data
$PrevLevel  = false;
$menu_tree_root  = 1;
$TBS->MergeBlock( 'blk', $cnx_id, 'SELECT node_name, Level FROM test ORDER BY LeftNode' );

//
// Now we need to close all level from the last record
$x   = '';
$ExtraRec  = array('Level'=>$FirstLevel);
m_blk_onsection('',$ExtraRec,$x,0);

$TBS->Show() ;

?>

HTML is inside a table

<table width=100%>
<tr>
<td>
[blk;block=begin;onsection=m_blk_onsection]
    [blk.open;htmlconv=no]
    [blk.node_name]
[blk;block=end]
[var.ExtraRec.close;htmlconv=no]
</td>
</tr>
</table>

By: Pirjo Posio
Date: 2007-10-24
Time: 02:19

Re: Tree structure

Hi andy,

The only error that I can see in your code is that TBS no longer has 'onsection' functions. Now we use 'ondata' instead. I tested this code, but had to use an array as data for the test. I also used <div> instead of a table to wrap the <ul><li>-list. Here's my code.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<html>

<head>

<title>TBS Tree for Modified Preordered Tree Traversal (MPTT by Celko)</title>

<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

</head>

<body>
<div>
[blk;block=begin;ondata=m_blk_ondata]
    [blk.open;htmlconv=no]
    [blk.node_name]
[blk;block=end]
[var.ExtraRec.close;htmlconv=no]
</div>
</body>

</html>
PHP:
<?php
include_once('../../../hidden/tbs_class_php5.php');
/* Data example for Modified Preordered Tree Traversal by Joe Celko
title                        node_id  parent_id      left_id  right_id  level

Fruit                        1                    0                1            16        0

+ Red                        2                    1                2             9        1

| + Cherry                4                    2                3             4        2

| + Strawberry            8                    2                5             6        2

| + Red apple            7                    2                7             8        2

+ Yellow                    3                    1                10            15        1

  + Lemon                5                    3                11            12        2

  + Banana                6                    3                13            14        2
*/
$menu0[] = array('node_name' => 'Fruit','level'=> 0, 'LeftNode' => 1);
$menu0[] = array('node_name' => 'Red',    'level'=> 1, 'LeftNode' => 2);
$menu0[] = array('node_name' => 'Cherry',    'level'=> 2, 'LeftNode' => 3);
$menu0[] = array('node_name' => 'Strawberry',    'level'=> 2, 'LeftNode' => 5);
$menu0[] = array('node_name' => 'Red Apple',    'level'=> 2, 'LeftNode' => 7);
$menu0[] = array('node_name' => 'Yellow',    'level'=> 1, 'LeftNode' => 10);
$menu0[] = array('node_name' => 'Lemon',    'level'=> 2, 'LeftNode' => 11);
$menu0[] = array('node_name' => 'Banana',    'level'=> 2, 'LeftNode' => 13);

$TBS = new clsTinyButStrong;
$TBS->LoadTemplate('tree.htm','UTF-8');

function m_blk_ondata( $Block, &$CurrRec, $RecNum )
{
global $FirstLevel, $PrevLevel;

if ( $PrevLevel === false ) {
  $FirstLevel  = $CurrRec['Level'] - 1;
  $PrevLevel  = $CurrRec['Level'] - 1;
}

$delta  = $CurrRec['Level'] - $PrevLevel;

if ( $delta > 0 ) {
  $CurrRec['open']  = "<ul><li>";
  $CurrRec['close']  = "";
} elseif ( $delta < 0 ) {
  $CurrRec['open']  = "</li>" . str_repeat('</ul></li>',max(0, -$delta)) . "<li>";
  $CurrRec['close']  = str_repeat('</li></ul>',max(0, -$delta));
} else {
  $CurrRec['open']  = "</li><li>";
  $CurrRec['close']  = "";
}
$PrevLevel  = $CurrRec['Level'];
}


//
// Merging data
$PrevLevel  = false;
$menu_tree_root  = 1;

$TBS->MergeBlock('blk',$menu0);
//
// Now we need to close all level from the last record
$x   = '';
$ExtraRec  = array('Level'=>$FirstLevel);
m_blk_ondata('',$ExtraRec,$x,0);

$TBS->Show() ;
?>
In real life one of course will use a database, like you were using. This Celko tree method is fast, when you only need to read the database once to get all the records for your tree, or branch of tree.
By: Andy
Date: 2007-10-24
Time: 03:26

Re: Tree structure

ondata makes it work right away. Thanks
By: Pirjo Posio
Date: 2007-10-24
Time: 03:52

Re: Tree structure

I want to resend correct version of my posting, to get the levels shown (and links):
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<html>

<head>

<title>TBS Tree for Modified Preordered Tree Traversal (MPTT by Celko)</title>

<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

</head>

<body>
<div>
[blk;block=begin;ondata=m_blk_ondata]
    [blk.open;htmlconv=no]

[blk;block=end]
[var.ExtraRec.close;htmlconv=no]
</div>
</body>

</html>
PHP:
<?php
include_once('../../../hidden/tbs_class_php5.php');
/* Data example for Modified Preordered Tree Traversal by Joe Celko
title            node_id  parent_id      left_id  right_id  level

Fruit                1          0          1         16     0

+ Red                2          1          2          9     1
| + Cherry           4          2          3          4     2
| + Strawberry       8          2          5          6     2
| + Red apple        7          2          7          8     2
+ Yellow             3          1         10         15     1
  + Lemon            5          3         11         12     2
  + Banana           6          3         13         14     2
*/
$menu0[] = array('node_name' => 'Fruit',      'level'=> 0, 'LeftNode' => 1);
$menu0[] = array('node_name' => 'Red',        'level'=> 1, 'LeftNode' => 2);
$menu0[] = array('node_name' => 'Cherry',     'level'=> 2, 'LeftNode' => 3);
$menu0[] = array('node_name' => 'Strawberry', 'level'=> 2, 'LeftNode' => 5);
$menu0[] = array('node_name' => 'Red Apple',  'level'=> 2, 'LeftNode' => 7);
$menu0[] = array('node_name' => 'Yellow',     'level'=> 1, 'LeftNode' => 10);
$menu0[] = array('node_name' => 'Lemon',      'level'=> 2, 'LeftNode' => 11);
$menu0[] = array('node_name' => 'Banana',     'level'=> 2, 'LeftNode' => 13);

$TBS = new clsTinyButStrong;
$TBS->LoadTemplate('tree.htm','UTF-8');

function m_blk_ondata( $Block, &$CurrRec, $RecNum )

{

    global $FirstLevel, $PrevLevel;

    if ( $PrevLevel === false ) {

        $FirstLevel = $CurrRec['level'] - 1;
        $PrevLevel = $CurrRec['level'] - 1;

    }

    $delta = $CurrRec['level'] - $PrevLevel;

    if ( $delta > 0 ) {

        $CurrRec['open'] = "<ul><li><a href='#'>" . $CurrRec['node_name'] . "</a>";
        $CurrRec['close'] = "";

    } elseif ( $delta < 0 ) {

        $CurrRec['open'] = "</li>" . str_repeat('</ul></li>',max(0, -$delta)) . "<li><a href='#'>" . $CurrRec['node_name'] . "</a>";
        $CurrRec['close'] = str_repeat('</li></ul>',max(0, -$delta));

    } else {

        $CurrRec['open'] = "</li><li><a href='#'>" . $CurrRec['node_name'] . "</a>";
        $CurrRec['close'] = "";

    }

    $PrevLevel = $CurrRec['level'];

}

// Merging data
$PrevLevel = false;

$menu_tree_root = 1;

$TBS->MergeBlock('blk', $menu0);


// Now we need to close all levels from the last record
$x = '';

$ExtraRec = array('level'=>$FirstLevel);


m_blk_ondata('',$ExtraRec,0);
$TBS->Show();

?>
By: TomH
Date: 2008-01-30
Time: 14:13

Re: Tree structure

Hi to all tree structure weird ones,

I was working on hierarchy menu tree and found both  the Modified Preordered Tree Traversal by Joe Celko and the parentage/lineage approach.

I chose to attempt the parentage/lineage approach and have added it to the "Tips and Tricks" part of the forum. -- your comments would be appreciated -- especially comparing the ease of maintenance and rebuilding  the lineage value for each item.

The actual demo pages and source code can be found at
http://tomhenry.us/tbs3/

Any ideas you care to contribute to improving my lineage approach,  would appreciate posting them on the "Tips and Tricks" thread.

Thanks for TBS,
TomH