Display an AJAX tree from your DB using CTreeView

14 followers

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).

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.

In your view, add:

<?php
$this->widget(
    'CTreeView',
    array('url' => array('ajaxFillTree'))
);
?>

This will create a tree widget CTreeView on your page, and its data will be fetch using the ajaxFillTree of the current controller.

So we have to add this action to the controller:

/**
     * 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()
    {
        if (!Yii::app()->request->isAjaxRequest) {
            exit();
        }
        $parentId = "NULL";
        if (isset($_GET['root']) && $_GET['root'] !== 'source') {
            $parentId = (int) $_GET['root'];
        }
        $req = 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"
        );
        $children = $req->queryAll();
        echo str_replace(
            '"hasChildren":"0"',
            '"hasChildren":false',
            CTreeView::saveDataAsJson($children)
        );
        exit();
    }

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 JS plugin that 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.

The exit() might not be necessary, but if you enabled the logs to Firebug or extensions like a debug toolbar, then Yii will write some JS at the end of your response, and it will break the JSON format.

Total 5 comments

#4382 report it
drumaddict at 2011/07/02 06:07pm
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

#3784 report it
macinville at 2011/05/08 04:36am
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.

#3608 report it
nacesprin at 2011/04/21 07:14pm
m1 and m2

As you can see, m1 and m2 are the same table "left joined"

#3606 report it
nettrinity at 2011/04/21 04:39pm
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?

#3376 report it
nacesprin at 2011/04/07 11:31am
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 "....

Leave a comment

Please to leave your comment.