The database ¶
This example uses a MySQL DB with a table named tree
with the fields id
, name
, and parent_id
. The parent_id
will be NULL
for root elements. The SQL is kept simple (no autoinc, no FK, etc).
[sql]
CREATE TABLE tree (
id INTEGER UNSIGNED NOT NULL,
name VARCHAR(50) NOT NULL,
parent_id INTEGER UNSIGNED,
PRIMARY KEY (id)
)
MySQL has no recursive queries. In order to fetch tree nodes, you have to recursively send SQL queries for each node, asking for its children. The easiest way to do so is to send these queries with AJAX, so that the tree can be displayed even if the deep nodes haven't been fetched yet. If you're using Oracle or Postgresql, there are other solutions, though this will also work.
The view ¶
In your view, add:
~
`
php
<?php
$this->widget(
'CTreeView',
array('url' => array('ajaxFillTree'))
);
?>
`
~
This will create a tree widget CTreeView on your page, and its data will be fetched using the ajaxFillTree
of the current controller.
The controller ¶
So we have to add this action to the controller:
~
`
php
/**
* Fills the JS tree on an AJAX request.
* Should receive parent node ID in $_GET['root'],
* with 'source' when there is no parent.
*/
public function actionAjaxFillTree()
{
// accept only AJAX request (comment this when debugging)
if (!Yii::app()->request->isAjaxRequest) {
exit();
}
// parse the user input
$parentId = "NULL";
if (isset($_GET['root']) && $_GET['root'] !== 'source') {
$parentId = (int) $_GET['root'];
}
// read the data (this could be in a model)
$children = Yii::app()->db->createCommand(
"SELECT m1.id, m1.name AS text, m2.id IS NOT NULL AS hasChildren "
. "FROM tree AS m1 LEFT JOIN tree AS m2 ON m1.id=m2.parent_id "
. "WHERE m1.parent_id <=> $parentId "
. "GROUP BY m1.id ORDER BY m1.name ASC"
)->queryAll();
echo str_replace(
'"hasChildren":"0"',
'"hasChildren":false',
CTreeView::saveDataAsJson($children)
);
}
When the page loads, an AJAX request will be sent to fill the first level of the tree. It'll have the GET parameter `root` set to `source` (this is the behavior of the [Treeview](http://bassistance.de/jquery-plugins/jquery-plugin-treeview/) JS plugin that [CTreeView](http://www.yiiframework.com/doc/api/1.1/CTreeView/) uses). We suppose here that the corresponding nodes (the root nodes) have a `parent_id` set to `NULL`. In SQL, "=" can't compare a value with `NULL`, so we have to use the `<=>` operator instead.
The other AJAX requests will have an integer value (the parent node's id) in `$_GET['root']`. We typecast this to "int" for security.
Then the code reads the data in the database. We need a *LEFT JOIN* to find if a node has children. If each row already has a `hasChildren` field, you can remove this join and your SQL will be faster.
When sending the JSON-encoded result, there's a little trick: the javascript wants the `hasChildren` attribute to be a boolean. But the SQL result just contains 0 or 1, so we have to convert it, at least for the "false" case. Instead of operating on the string result, one could modify the PHP array with a `foreach`.
### If AJAX fails
If your AJAX query fails, you should try to debug it in your browser. Firefox, Chrome and Opera have good developer toolbars that will show you the AJAX requests sent and what their answers were. You may find that some HTML is appended at the end of the expected JSON answer. I had this problem with Yii'JS logs and a toolbar extension. In this case, insert an `exit();` at the end of the action. Or even better: `Yii::logger->flush(); exit();`.
### Final note
This is meant as a simple introduction. There are many ways to enhance this:
* Refactor the controller action by putting its 2 last lines into a model.
* Increase the abstraction level, so that the code can be used on various models, as long as they share the same table structure.
* If the tree content rarely changes, you may consider using a different structure, like nested sets. This will make most queries easier, e.g. fetching the path of a node.
Handling onclick action
In order to be able to get the ID of the record, you can achieve changing a bit the TEXT selection field as follow:
..."SELECT m1.id, CONCAT('<span onclick=\"alert(\'',m1.id,'\')\">',m1.name,'</span>') AS text, m2.id IS NOT NULL AS hasChildren "....
what is m1 and m2 here?
"SELECT m1.id, m1.name AS text, m2.id IS NOT NULL AS hasChildren "
not quite understand m1 and m2 here. Can you help me explain?
m1 and m2
As you can see, m1 and m2 are the same table "left joined"
Displaying image and links
I wanted to use this feature as a side navigation menu, so it should be able to create links in it.
Here I have posted my working solution (I just did some minor modifications with the controller).
Hope I was able to help someone.
Why not use nested set
For hierarchical structured data I think the best solution is to use the nested set model.
There are several extensions for Yii,for example:
http://www.yiiframework.com/extension/nestedsetbehavior
Business logic in controller
Why do you have business logic in your controller?
Most of what I see in your example belongs in a model.... MVC, you know?
Answers
@Backslider
This is a wiki, you know. Instead of useless comments, you could enhance this article.
I'm not sure splitting this fat controller into a controller and a model would make the article easier to follow. This is meant as a simple how-to, so I'd rather keep this light introduction and suggest at the end to refactor the 3 last lines of the action into a model.
@rumaddict
Nested sets have a different use case. They make insertion difficult, so one shouldn't use them on changing data.
Their concept is also bit harder, so I prefer to use this simple structure in this small Yii tutorial.
yii treeview collasped issue
Sir please give me a tip that how can i get the expanded treeview on the first load of the page and not collapsed, i changed the collapsed property to false and true but it has no effect.
Non AJAX trees
@Amjad Khan
You should use the forum to ask such questions. A few tips:
data
property of CTreeView.return empty array when add some condition
I am using this query to get records for tree view and add some condition in it, but it return empty array (safety_deposit_box_id have NULL in databse)
table fields:
id, name ,parent_id, users_idsafety_deposit_box_id, created_date, updated_date
SELECT m1.id, m1.name AS text,m1.parent_id, m2.id IS NOT NULL AS hasChildren FROM finance_portfolio_folders AS m1 LEFT JOIN finance_portfolio_folders AS m2 ON m1.id=m2.parent_id WHERE m1.parent_id <=> 1 AND m1.users_id=1 AND m1.safety_deposit_box_id IS NULL GROUP BY m1.id ORDER BY m1.name DESC
When I remove the "AND m1.safety_deposit_box_id IS NULL " all records show properly
I want to add these conditions in query
m1.users_id=$users_id AND m1.safety_deposit_box_id IS NULL
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.