<List Articles>Drag and Drop Trees With CakePHP
The "TreeBehavior" built into CakePHP has a number of useful applications beyond the Auth/ACL uses that most developers use it for. As such, it's good to have a way to manipulate the tree relationships in an intuitive, user-friendly way. The goals in this particular project were to create a drag & drop UL/LI list that would retain not only the parent relationships, but also the sibling relationships.
In this example, let's create an organizational chart. This is something you'd see that demonstrates the relationships amongst employees at an organization - something that could potentially change over time.
Prepping The Database
First, obviously, we need a table. Here's what I'm using in this example:
CREATE TABLE `positions` ( `id` mediumint(9) NOT NULL auto_increment, `position_name` varchar(100) default NULL, `parent_id` mediumint(9) NOT NULL default '0', `lft` mediumint(9) NOT NULL default '0', `rght` mediumint(9) NOT NULL default '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=75 DEFAULT CHARSET=utf8;
Go ahead and create this table first, then go to your CakePHP application directory and use the bake shell to create the model.
# bake Welcome to CakePHP v1.2.0.7125 RC1 Console --------------------------------------------------------------- App : app Path: /[path/to]/demo/app --------------------------------------------------------------- Interactive Bake Shell --------------------------------------------------------------- [D]atabase Configuration [M]odel [V]iew [C]ontroller [P]roject [Q]uit What would you like to Bake? (D/M/V/C/P/Q) > M --------------------------------------------------------------- Bake Model Path: /[path/to]/demo/app/models/ --------------------------------------------------------------- Possible Models based on your current database: 1. Position Enter a number from the list above, type in the name of another model, or 'q' to exit [q] > 1 Would you like to supply validation criteria for the fields in your model? (y/n) [y] > n Would you like to define model associations (hasMany, hasOne, belongsTo, etc.)? (y/n) [y] > n --------------------------------------------------------------- The following Model will be created: --------------------------------------------------------------- Name: Position Associations: --------------------------------------------------------------- Look okay? (y/n) [y] > y Baking model class for Position... Creating file /[path/to]/demo/app/models/position.php Wrote /[path/to]/demo/app/models/position.php Cake test suite not installed. Do you want to bake unit test files anyway? (y/n) [y] > y You can download the Cake test suite from http://cakeforge.org/projects/testsuite/ Baking test fixture for Position... Creating file /[path/to]/demo/app/tests/fixtures/position_fixture.php Wrote /[path/to]/demo/app/tests/fixtures/position_fixture.php Baking unit test for Position... Creating file /[path/to]/demo/app/tests/cases/models/position.test.php Wrote /[path/to]/demo/app/tests/cases/models/position.test.php ---------------------------------------------------------------
Now that we've got our model built, we need to identify it as a "Tree." We'll do this using the Behavior mechanism in CakePHP. All we're adding is the following line inside our model.
var $actsAs = array('Tree');
You can go ahead and use the bake console to create the controller and views for this model. Go ahead and create two records via the generated CRUD pages - just don't give any values for parent/lft/right. Both records will be top-level nodes in the tree.
Creating The View
The next step is creating an HTML ul/li list of the tree so we can see what it looks like. There is already a TreeBehavior::generatetreelist() function for creating a semi-useful array with each node padded for each parent. This is only marginally useful, as it doesn't look very good in HTML output at all. Instead, we'll make use of this function to create a ul/li list showing the relationships. For this, we'll use a Helper. Inside your app/views/helpers/ directory you can use the following code. In this example I'm calling it TreeViewHelper, which would be inside a file called tree_view.php.
(Note: Using TreeBehavior::generatetreelist() probably isn't the best way to do this - the best way would be using code similar to output the ul/li list directly, but for several reasons I chose to do it this way. Your mileage may vary.)
<?php
class TreeViewHelper extends Helper {
function createListTree($tree) {
$out = '';
$depth = 0;
$prev_depth = 0;
$count = 0;
foreach ($tree as $id => $node) {
$depth = strrpos($node, '_');
if ($depth === false) {
$depth = 0;
$clean_node = $node;
} else {
$depth = $depth + 1;
$clean_node = substr($node, strrpos($node, '_')+1);
}
if ($depth > $prev_depth) {
$out .= "\n<ul>\n";
} else if ($depth < $prev_depth) {
for ($i = 0; $i < ($prev_depth-$depth); $i++) {
$out .= "</li></ul>\n";
}
} else if ($count>0) {
$out .= "\n</li>\n";
}
$out .= '<li id="node_' . $id . '" noDelete="true" noRename="true"><a class="drag" href="/positions/view/' . $id . '/' . $id . '/">' . $clean_node . '</a>';
$out .= "\n";
$prev_depth = $depth;
$count++;
}
for ($i = 0; $i < ($depth); $i++) {
$out .= "</li></ul>\n";
}
if (!empty($tree)) {
$out .= '</li>';
}
return $out;
}
}
Inside our controller, we'll need to add the TreeView helper.
var $helpers = array('Html', 'Form', 'TreeView');
Create a controller function for our view (I'm using 'treeview') and then the view itself (/app/views/positions/treeview.ctp).
Here's the beginnings of our function:
function treeview() {
$this->set('positions', $this->Position->generateTreeList());
}
And the view:
<ul id="role_tree">
<?php
echo $treeView->createListTree($positions);
?>
</ul>
If you load the page, viola! A tree! The only problem is that you're probably seeing ID's, not the 'Position Name.' This is easily solved by adding this to your model:
var $displayField = 'position_name';
Refresh the page, and now you should be seeing your two sibling records in a ul list.
Drag & Drop
Now for the drag and drop. Instead of reinventing the wheel on this one we're going to use some code that DHTML Goodies has made available for use. It's not the cleanest of all possible code, but it works for our purposes.
I downloaded the zip file and unzipped it in /app/webroot/js/dragdrop/. It contains all the images, etc, necessary for this project. If you want to make changes to it, you'll need some familiarity with Javascript and CSS, which I assume you have.
Once it's unzipped, we'll include it in our view. I've already added the necessary 'node' tags to the HTML output, so all you'll need to do is update your view so it looks like this:
<script type="text/javascript" src="/js/dragdrop/js/ajax.js"></script>
<script type="text/javascript" src="/js/dragdrop/js/context-menu.js"></script>
<script type="text/javascript" src="/js/dragdrop/js/drag-drop-folder-tree.js"></script>
<link rel="stylesheet" type="text/css" href="/js/dragdrop/css/drag-drop-folder-tree.css" />
<link rel="stylesheet" type="text/css" href="/js/dragdrop/css/context-menu.css" />
<a href="javascript:saveNodes();">Save Changes</a>
<ul id="tree" class="dhtmlgoodies_tree">
<?php
echo $treeView->createListTree($positions);
?>
</ul>
<script language="javascript">
window.onload=function() {
treeObj = new JSDragDropTree();
treeObj.setTreeId('tree');
treeObj.setMaximumDepth(15);
treeObj.setMessageMaximumDepthReached('Maximum depth reached');
treeObj.setImageFolder('/js/dragdrop/images/');
treeObj.initTree();
treeObj.expandAll();
};
function saveNodes() {
save_string = treeObj.getNodeOrders();
location.href='/positions/save/?save_string=' + save_string;
}
</script>
If you reload the page, you should now be able to drag & drop nodes. The next step is to save the changes. I've already added the link and the javascript function to the view. Note that for the purposes of this particular demonstration, I'm not using the AJAX method that is available. It's pretty easy to change that, if you wish to.
We're going to add a new function to our controller.
function save() {
// First get the tree in it's original format.
$tree = $this->Position->generateTreeList();
// Save string looks like ID-PARENTID,ID-PARENTID,....
$save_string = $this->params['url']['save_string'];
$associations = explode(',', $save_string);
foreach ($associations as $relationship) {
$ids = explode('-', $relationship);
$saved_ids[] = $ids[0];
$this_node_id = $ids[0];
$this_node_parent_id = $ids[1];
$this->Position->recursive = -1;
$position = $this->Position->find('first', array('conditions' => array(
'Position.id' => $this_node_id,
)
));
$this->Position->set($position);
if (isset($position['Position']['id']) && $position['Position']['parent_id'] !== $this_node_parent_id) {
$ret = $this->Position->setParent($this_node_parent_id);
if ($ret === false) {
$this->Session->setFlash('Failed to save the organizational chart.');
$this->redirect(array('action' => 'treeview'));
break;
}
}
}
if (array_keys($tree) != $saved_ids) {
// Need to reorder based on sibling heirarchy changes!
$count = count($saved_ids) - 1;
for ($i = $count; $i >= 0; $i--) {
$ret = $this->Position->moveUp($saved_ids[$i], true);
// Returns false when we move an already-at-top to the top, so ignore falses.
}
}
$this->Session->setFlash('Saved changes.');
$this->redirect(array('action' => 'treeview'), null, true);
}
Once you've saved this in your controller, you should be able to click the "Save Changes" link on the view and it will work properly. To create more nodes, simply add new top-level records; after creating them you'll be able to drag them anywhere in the hierarchy.
Questions, Comments?
I'm not sure how this would work if you had two people saving changes at the same time. I'm fairly sure it wouldn't be pretty.
Other than that, it seems to work pretty well. I haven't noticed a significant slow-down when there are hundreds of records that must be re-arranged (you may notice that saving the changes can result in a lot of database updates).
If you have any comments, criticisms, etc, please feel free to share them. Hopefully this will be helpful to others who were trying to accomplish similar goals as I was in working on this project.

Great job man! Keep working like that!
Can u create a function emitate WIndows Explorer tree view? I hope to see that soon!
^_^ sorry for my noob English