Yii 1.1: jstreeinputwidget

Input widget for JStree
7 followers

This is a CInputWidget form of the jstree JQuery widget.

This is my first extension and I have only been using Yii for about a month now so be gentle :).

I basically made this since the other jsTree widget is not only down voted a lot but is also using a version of jsTree that is far outdated.

WARNING: This widget is extremely basic and is only designed to do input widget handling. It has not yet got the capability to do tree manipulation, however, I am planning on adding the functionality.

In theory to add tree manipulation to this plugin is possible due to the new nature in which jsTree now utilises plugins and how my plugins array is a direct translation to the jsTree construct. So if you feel adventerous and decide to add tree manipulation then let me know and I'll add it to the widget.

Despite this widget being basic it has potiential and I wouldn't mind if someone with more time and Yii knowledge on their hands would take it from me and produce a reputable, well made and comprehensive plugin for everyone :).

Requirements

  • Yii 1.1.7 (tested, could probably go lower but I have not tested on other versions)
  • jsTree 1.0-rc3
  • nestedsetbehavior plugin from this site (By samdark)

Usage

This widget is best used within a many->many database relationship between a table of nestedsetbehavior hierachal data and a record within another table (i.e. a product). Make sure you have an intermediate table between the two to define a normalised relationship else this widget could get quite hard to implement.

An example of what I mean is:

  • A title/product can have many geozone classifications
  • A Geozone can be classified with many titles
  • So let's make a TitleGeozone table between them to normalise the relationship

Currently the best way to add data to this widget is through the HTML_DATA plugin (link). An example of gathering a multi-root tree (created by the nestedsetbehavior) is like so:

public function getTreeUL($selected_vals = array()){
    $tree = '';
    $tree .= CHtml::openTag("ul")."\n"; // Open the list
 
    foreach(self::model()->findAll(array("group"=>"root")) as $root){ // Gte each root one by one
 
        $node_level=0; // Start at 0 level
        $root_tree = self::model()->findAll(array(
            'condition'=>'root=:root',
            'params'=>array(":root"=>$root->id),
            'order'=>'t.left',
        )); // Get the tree for this root.
 
        // Do not display the root here as it will be displayed in the node loop
 
        foreach($root_tree as $n=>$node) // Loop through the tree
        {
            if($node->level==$node_level)
                $tree .= CHtml::closeTag('li')."\n"; // close the last node since we are onto a new one on the same lvl
            else if($node->level>$node_level)
                $tree .= CHtml::openTag('ul')."\n"; // This is a sub list so make new ul
            else
            {
                $tree .= CHtml::closeTag('li')."\n"; // end of sublist to close everything
                $tree .= CHtml::closeTag('ul')."\n";
                $tree .= CHtml::closeTag('li')."\n";
            }
 
            $class= "";
            if(array_key_exists($node->id, $selected_vals)){ // This contains an array like so array(1=>1, 4=>1) with the indexes being geozone ids of selected geozones.
                $class .= "jstree-checked"; // If the parent node is selected show its children.
            }
 
            $tree .= CHtml::openTag('li', array("id"=>"geozone_classification_tree_".$node->id, "class"=>$class)); // Open the node
            $tree .= CHtml::link(CHtml::encode($node->caption)); // Add a link
            $node_level=$node->level; // Make the lvl equal the previously seen one
        }
 
        for($i=$node_level;$i;$i--) // End of list so close the list
        {
            $tree .= CHtml::closeTag('li')."\n";
            if($i > 1){ $tree .= CHtml::closeTag('ul')."\n"; }
        }
    }
 
    $tree .= CHtml::closeTag("ul")."\n"; // close the list
    return $tree; //return the ul ready for use
}

And an example of getting a single root tree

function getSingleTree($selected_vals = array(), $root_id = null){
 
    $tree = "";
    if(!$root_id){
        $root_tree = self::model()->findAll(array(
            'order'=>'t.left',
        )); // Table is one tree and only one tree
    }else{
        $root_tree = self::model()->findAll(array(
            'condition'=>'root=:root',
            'params'=>array(":root"=>$root_id),
            'order'=>'t.left',
        )); // Get the tree for this root.
    }
    $level=0;
 
    foreach($root_tree as $n=>$node)
    {
        if($node->level==$level)
            $tree .= CHtml::closeTag('li')."\n";
        else if($node->level>$level)
            $tree .= CHtml::openTag('ul')."\n";
        else
        {
            $tree .= CHtml::closeTag('li')."\n";
            $tree .= CHtml::closeTag('ul')."\n";
            $tree .= CHtml::closeTag('li')."\n";
        }
 
        $class= "";
        if(array_key_exists($node->id, $selected_vals)){ // This contains an array like so array(1=>1, 4=>1) with the indexes being geozone ids of selected geozones.
            $class .= "jstree-checked"; // If the parent node is selected show its children.
        }
 
        $tree .= CHtml::openTag('li', array("id"=>"geozone_classification_tree_".$node->id, "class"=>$class)); // Open the node
        $tree .= CHtml::link(CHtml::encode($node->caption)); // Add a link
        $level=$node->level;
    }
 
    for($i=$level;$i;$i--)
    {
        $tree .= CHtml::closeTag('li')."\n";
        $tree .= CHtml::closeTag('ul')."\n";
    }
 
    return $tree;
}

You may notice on this line:

$class .= "jstree-checked";

I use specific classes on my li node to tell jsTree what to do. Using jsTree-checked on nodes previously selected is vital since my own widget takes these selected nodes and adds them to a hidden JSON input field for initial use. Other classes can be used to change the tree behaviour some of which can be found here. My Widget will also accept the other form of opening a tree node by using:

"core" : { "initially_open" : [ "root" ] },

To display the tree itself put this extension into your extensions folder and reference it like so:

$this->widget("application.extensions.widgets.jstree.JSTree", array(
    "plugins"=>array(
        "html_data"=>
            array("data"=>$model->getGeozoneTreeUL($model->getGeozone_ids())),
        "checkbox"=>
            array(
                        "real_checkboxes"=>true,
                        "two_state"=>true,
                        "real_checkboxes_names"=>'js:function (n) {
                           var val = n[0].id.replace(/geozone_classification_tree_/, "");
                           return [("check_geozone_" + (n[0].id)), val];
                         }'),
            "themes"=>array( "theme" => "default" ),
            "sort",
            "ui"
    ),
    "model"=>$model->getTitleGeozoneModel(),
    "attribute"=>"geozones_id"
))

This shows a basic example of how to display a geozone tree allowing the user to select multiple geozones to classify an object (i.e. a product).

The first thing you will notice is the main plugins array. This is a 1-1 relationship with the main constructor of jsTree and is flexible enough to allow you to change it with new plugins for the jsTree JQuery plugin.

Items within the plugins array that do not have configuration data with them are plugins that will be loaded "as is" into the jsTree plugin.

A full list of acceptable params and entries for the plugins array can be found at the jsTree documentation site.

This widget returns a JSON array within the attribute you defined in the widget params in the form of:

["1", "3", "7"]

Where each entry is a selected node value garnished from the real_checkboxes_names param in the widget. This is basic functionality of the jsTree JQuery plugin widget and all I do is push that value into a hidden field.

You can then save this attribute by using a batch save. The batch save for the running example I have shown you looks something like:

$valid = true;
$geozones = array();
$titleGeozones = json_decode($_POST['TitleGeozones']['geozones_id']) ? json_decode($_POST['TitleGeozones']['geozones_id']) : array();
 
foreach($titleGeozones as $geozone){
 
        $geozoneModel = new TitleGeozones();
    $geozoneModel->title_id = $model->id;
    $geozoneModel->geozones_id = (int)$geozone;
    $valid=$geozoneModel->validate() && $valid;
 
    $geozones[] = $geozoneModel;
}
 
if($valid){
    $model->deleteAllGeozones();
    foreach($geozones as $geozone){
        $geozone->save();
    }
}

Speeding up the JSTree widget with JSON and progressive_render

Sometimes when you have a huge tree you will find that loading the entire tree using the html_data plugin can actually crash or freeze your browser for a short while. This is because most browsers are only 32bit and cannot handle more than a certain amount of RAM at any given point in time. This is a good thing since the last thing you wanna do is use up all 8GB of the users RAM rendering your tree.

So lets talk about speeding up this widget for large trees. You can load the entire tree in as a JSON string and use the json_data plugins progressive_render and/or progressive_unload (link to documentation) to allow you to load as little DOM as humanly possible.

Here is a short example of how to load a "Geozone" tree (from within the Geozone model):

function getJSONTree(){
 
    $tree = array();
 
    foreach(self::model()->findAll(array("group"=>"root")) as $root){
        $tree_array = array('data' => $root->caption);
        $children_for_this_node = $root->getChildren();
 
        if(count($children_for_this_node) > 0){
            $tree_array['children'] = $children_for_this_node;
        }
 
        $tree[] = $tree_array;
    }
    return $tree;
}
 
function getChildren(){
    $children = array();
    $this_children = $this->children()->findAll();
        foreach($this_children as $i => $child){
            $child_array = array('data' => $child->caption);
            $children_for_this_node = $child->getChildren();
 
            if(count($children_for_this_node) > 0){
                $child_array['children'] = $children_for_this_node;
            }
            $children[] = $child_array;
        }
    //var_dump($children);
    //exit();
    return $children;
}

With these functions you can call getJSONTree() into the plugins array of the widgets delcaration like so:

"plugins"=>array(
    "json_data"=>
        array(
        "data"=> $model->getJSONTree(),
        "progressive_render" => true
        ),
    "checkbox"=>
        array(
        "real_checkboxes"=>true,
        "two_state"=>true,
        "checked_parent_open"=>true,
        "real_checkboxes_names"=>'js:function (n) {
            var val = n[0].id.replace(/geozone_classification_tree_/, "");
            return [("check_geozone_" + (n[0].id)), val];
        }'
    ),
    "themes"=>
        array( "icons" => false ),
    "sort",
    "ui"
),

Notice how I call the progressive_render in the json_data plguin array too? Remember the declaration in this plugin is a 1-1 to the actual JS script so you can keep adding new params as they come out in the same format as it shows in the documentation.

Hope this helps speed up things for you.

v0.1 and onwards Addition: Event Binding.

You can now bind events with the specific JSTree in question directly from the plugin declaration. An example of how this works is below:

<?php $this->widget("application.extensions.widgets.jstree.JSTree", array(
    "plugins"=>array(
        "html_data"=>
        array(
            "data"=>$model->getGeozoneTreeUL($model->getGeozone_ids())
        ),
        "checkbox"=>
            array(
                "real_checkboxes"=>true,
                "two_state"=>true,
                "checked_parent_open"=>true,
            "real_checkboxes_names"=>'js:function (n) {
                var val = n[0].id.replace(/geozone_classification_tree_/, "");
                return [("check_geozone_" + (n[0].id)), val];
            }'
        ),
        "themes"=> array( "icons" => false ),
        "sort",
        "ui"
    ),
    "bind" => array(
        'uncheck_node.jstree' => 'function(e, data){
 
            var obj = data.rslt.obj.children(":checkbox");
 
            if($(this).parent().children("input").val() == "" || $(this).parent().children("input").val() == null){
                $c_selected = []; // if empty make new object
            }else{
                $c_selected = JSON.parse($(this).parent().children("input").val()); // Get all currently selected in this list
            }
 
            $("#Title_main_geozone_id option[value="+obj.val()+"]").remove();
        }',
        'check_node.jstree' => 'function(e, data){
            var obj = data.rslt.obj.children(":checkbox");
 
            if($(this).parent().children("input").val() == "" || $(this).parent().children("input").val() == null){
                $c_selected = []; // if empty make new object
            }else{
                $c_selected = JSON.parse($(this).parent().children("input").val()); // Get all currently selected in this list
            }
            $("#Title_main_geozone_id").append("<option value=\""+obj.val()+"\">"+data.rslt.obj.children("a").text()+"</option>");
        }'
    ),
    "model"=>$model->getTitleGeozoneModel(),
    "attribute"=>"geozones_id"
)) ?>

Here I use the Bind parameter in the widget declaration to assign two differnt events:

  • uncheck_node
  • check_node

I then hook into these events with a function which basically just adds the selected node to a selectbox (or retrospectively removes that option from the selectbox). This sort of functionality can be very handy when you are trying to pick "main" node out of many selected ones.

This is just a very basic example but still one that works for real.

You will notice that the two params, just like the construct, are direct translations to their JS counterparts. This is to increase future compatibility with new events etc etc that come out in JSTree allowing the JS of the widget to be updated without needing to update the widget itself so that even if I do not update the JSTree version of this widget it should be pretty easy for you to keep this widget always using the latest version of the script.

Change Log

2011-11-23: Sammaye

  • Added an example of using this with a multi-root JSON based Tree using lazy rendering to speed up the widget

2011-11-08: Sammaye

  • Added the ability to attach functions to the tree events from within the Plugin declaration.
  • Added a bug fix to stop the load event in the widget from effecting other events.

2011-07-18: Sammaye

  • Added bug fixes into the initial launch. Added the missing remove() function and removed deprecated variables.
  • Corrected error in docs with class assignment on node li's
  • Initial Beta release

Total 1 comment

#12489 report it
Nur Rochim at 2013/03/23 12:11pm
DEMO

please demo page

Leave a comment

Please to leave your comment.

Create extension