Skip to main content

Using CTreeView with NestedSetBehavior in Yii

Getting this whole thing to work took me a few days of experimenting. Finally I've got it working. Posting it here hoping that it would save others some time and if I need it back in the future.

The Yii nested set behavior extension can be found at http://www.yiiframework.com/extension/nestedsetbehavior/.

Download the zip file and then extract it into the "app root"/protected/extensions/NestedSetBehavior folder. Make sure that the NestedSetBehavior.php can be found at "app root"/protected/extensions/NestedSetBehavior/NestedSetBehavior.php.

Then modify the model file which you want to use the behavior with. For example, I have a model Areas and the file "app root"/protected/models/Areas.php. Add to the mentioned file the following lines:
 public function behaviors()
 {
  return array(
   'nestedSetBehavior'=>array(
    'class'=>'application.extensions.NestedSetBehavior.NestedSetBehavior',
    'leftAttribute'=>'lft',
    'rightAttribute'=>'rgt',
    'levelAttribute'=>'level',
    'hasManyRoots'=>true,
    'rootAttribute'=>'root_area',
   ),
  );
 }

The table for the model has to have the lft, rgt, level, and root_area field. All the field of type integer. The hasManyRoots determine whether the behavior should support more than 1 root per table, and so if it is true, the root_area will contain the root node id.

Then you need to modify the actionCreate and the actionUpdate method of the controller to use $model->saveNode(); rather than $model->save();

And that should be it to get nested behavior working. But.... how would you display it using the CTreeView widget? And how would you update it using that?

To display the tree, in the view, it is as simple as:
$this->widget('CTreeView', array(
 'id'=>'treelink',
 'data'=>$treedata,
)); 

But to get the $treedata? I've added the following 2 function into the model file.
 
public function treechild($id)
 {
  $curnode = Areas::model()->findByPk($id);
  if($curnode){
   $childrens = $curnode->children()->findAll();
   if(sizeOf($childrens)>0){
    $out = array();
    foreach($childrens as $children){
     $currow=['id'=>$children->id,'text'=>$children->name,'children'=>$this->treechild($children->id)];
     $out[]=$currow;
    }
    return $out;
   }
   else{
    return null;
   }
  }
  return null;
 }

 public function treedata()
 {
  $roots = Areas::model()->roots()->findAll();
  $out = array();
  foreach($roots as $root){
   $currow=['id'=>$root->id,'text'=>$root->name,'children'=>$this->treechild($root->id)];
   $out[]=$currow;
  }
  return $out;
 }


The treedata function would iterate over all the root nodes in the table and the treechild function would iterate over all the child nodes so that the data would be put into a properly formatted data structure. The CTreeView require an array with at least the id field for id, and text field to be displayed in the tree.

And so the controller is as simple as:
  $this->render('tree',array(
   'dataProvider'=>$dataProvider,
   'treedata'=>Areas::model()->treedata(),
  ));


But that's only good for displaying the tree, to fully modify the tree (add new nodes and updating existing nodes), I need to add the following javascript to the view:
Yii::app()->clientScript->registerScript('clickscript',"
$('#treelink li').on('click', function(event) {
 event.stopPropagation();
 $('#create').attr('href',$.param.querystring($('#create').attr('href'),'parent_id='+$(this).attr('id')));
 $('#edit').attr('href',$.param.querystring($('#edit').attr('href'),'id='+$(this).attr('id')));
});
",CClientScript::POS_READY);


And the following menu (do note that I'm currently using the default layout which displays the menu widget):
$this->menu=array(
 array('linkOptions'=>array('id'=>'create'),'label'=>'Create Areas', 'url'=>array('create')),
 array('linkOptions'=>array('id'=>'edit'),'label'=>'Edit Area', 'url'=>array('update')),
 array('label'=>'Manage Areas', 'url'=>array('admin')),
);


And in the create view:
if(isset($parent_id)){
echo $this->renderPartial('_form', array('model'=>$model,'parent_id'=>$parent_id)); 
}

And in the _form view:
if(isset($parent_id)){
 echo CHtml::hiddenField('parent_id',$parent_id);
}


And in the actionCreate method in the controller, added after the "$model->attributes=$_POST['Areas'];" line:
   if(isset($_POST['parent_id'])){
    $parent = Areas::model()->findByPk($_POST['parent_id']);
    if($parent){
     $model->appendTo($parent);
    }
   }


And the render line would be changed to:
  $this->render('create',array(
   'model'=>$model,
   'parent_id'=>$_GET['parent_id'],
  ));


So what the whole thing actually did, was to bind the li items in the node tree to change the create and edit link when clicked. The create link will be changed so that the currently selected node id set as parent_id and the edit link is changed so that id is the currently selected node id. Once the create method received the parent_id variable, it would be passed on all the way into the form. And once saved, if the parent_id actually exists and points to a real node, the new node would be set to append to the aforementioned node. There isn't much change for the update method.

And that folks, is how you would use the CTreeView with the NestedSetBehavior.

Comments

Anonymous said…
That extension is SOOOO poorly documented. The methods are all named to do things other than what they actually do. It's a shame because it's clearly a lot of hard work, but half-ass documentation makes it a real pig to work with.
Anonymous said…
Poorly documented?
You mean NestedSetBehaviour?

Actually it is pretty well documented.
ROFL - stop complain, start to read.

@Author
Thanks for your article.

Popular posts from this blog

Dell Inspiron Mini 9

Yesterday went to Low Yat to buy my cousin a sub RM 1500 notebook. Of course with that kind of price tag it would have to be a netbook jelah. Anyhow, I was short on time so I didn't get to browse thoroughly. Just went for one quick round around level 2 and picked the one that I thought would fulfil his need. The thing is, as soon as I stepped out of the lift there was a shop that sold the new Dell Inspiron Mini 9 for only RM 1199. I was suprised. Just 1199. I couldn't believe my eyes. With original Windows XP and all. RM 1199. I thought there must be something wrong. Better survey first. So went around level 2. There was nothing that could match that price. Most of the other netbook from acer, asus and lenovo even was around RM 1600. So I decided I'll just go with the Dell lah. But to tell you the truth if I had money to buy for my own use, I'd go with the Lenovo Ideapad S10. Fuh.. she's one sweet mama... Smooth rounded curves, slim white profile. Pergh.. But anyway

Installing Centos 7 on HP G7 Server

When I first initially tried it, the installer cannot detect any local hard disk to install to. After much googling found out that it was because a certain driver for the Smart Array Controller is no longer available on Centos 7. To allow for use of the new driver on old hardware, you need to enable the option. Once the DVD has booted and on the first menu screen, highlight the first option and then press tab which would bring you to a command line where you can add additional parameters to boot. Add the following parameters: hpsa.hpsa_allow_any=1 hpsa.hpsa_simple_mode=1 And then press enter to boot. You should now be able to install as you wish. After installation, on the first boot, make sure at the menu, press 'e' to edit the parameters and add them again. Then once booted, go to /boot/grub2/grub.cfg to permanently add the parameters to the menu. Refer to http://serverfault.com/questions/611182/centos-7-x64-and-hp-proliant-dl360-g5-scsi-controller-compatibility and it&#

haproxy on Centos 7 with SELinux

Setting up haproxy and varnish with reference to the configuration from these sites: http://sharadchhetri.com/2014/12/20/how-to-install-varnish-4-version-on-centos-7-rhel-7/ https://www.upcloud.com/support/haproxy-load-balancer-centos/ And finally can access plone on the server. But when I check the haproxy status, only client1 can be accessed. All other clients were down with general socket error, permission denied. Googling a bit I finally found out it was due to SELinux. Refer to this page: http://stackoverflow.com/questions/26420729/haproxy-health-check-permission-denied With the solution from that page, I installed policycoreutils-python and tried to run the command given: semanage port --add --type http_port_t --proto tcp 8081 But that command failed with the error that the port was already defined. When I looked it up using: semanage port -l | grep 8081 It belonged to some other type (transproxy_port_t). When I tried to delete the type using: semanage port -d -t transprox