Linked listBox!

Hello guys!

I’ve already read alot about dropDrownList, how to fill the values, get values, dependents dropDownList, the wiki, etc etc… but im not able to finish one thing that im trying to do for almost 2 days…

Imagine this scenario:

I got 1 table (tbl_category) that can have a ‘infinite’ number of categories. For that, i have done the typical struct (id, parent_id, …) where parent_id is linked to id, e.g.




+----+-----------+----------------+

| id | parent_id | name           |

+----+-----------+----------------+

|  1 |      NULL | Motors         | Parent

|  2 |      1    | Cars           | Child

|  3 |      2    | Vans           | GrandChild

+----+-----------+----------------+



i.e, we have the subsubcategory Vans, that belongs to subcategory Cars and Cars that belongs to category Motors.

Ok, so far so good, but now comes the problem for me…i want to make 3 listBox linked it self, i. e. 1st listBox for categories, 2dn listBox for subcategories and a 3rd listBox for the subsubcategories.

The 1st one is filled when the page is loaded, the second is filled when i clicked on the category and the 3rd is filled only when the user click on the subcategory (2nd listBox). All the examples that i read, works in a different way, i.e. we clicked on the 1st listBox and the others two or more are filled automatically via JSON.

The really problem is when i click on the 2nd, nothing happens…showing some code:

Controller




...

public function accessRules()

{

return array(

                        ...

                        array('allow', 

                                'actions' => array('create', 'updatechild', 'updategrandchild'),

                                'users' => array('@'),

                        ),

                        ...

                );

}

...

// ListBox LVL 2

public function actionUpdateChild()

{

                // Childs

                $data = Category::model()->findAll('parent_id=:category_id', array(':category_id' => (int) $_POST['parent_id']));

                $data = CHtml::listData($data, 'id', 'name');                

                

                //$listBoxChilds = "<option value=''>Select Childs</option>";

                foreach($data as $id => $value)

                        //$listBoxChilds .= CHtml::tag('option', array('value' => $id), CHtml::encode($value), true);

                        echo CHtml::tag('option', array('value' => $id), CHtml::encode($value), true);

                

                /*

                // GrandChilds

                $listBoxGrandChilds = "<option value=''>Select Grand Childs</option>";

                

                // return data (JSON)

                echo CJSON::encode(array(

                        'listBoxChilds' => $listBoxChilds,

                        'listBoxGrandChilds' => $listBoxGrandChilds

                ));*/

}

        

// ListBox LVL 3

public function actionUpdateGrandChild()

{

                $data = Category::model()->findAll('parent_id=:category_id', array(':category_id' => (int) $_POST['child_id']));

                $data = CHtml::listData($data, 'id', 'name');

                

                //$listBoxGrandChilds = "<option value=''>Select Grand Childs</option>";

                foreach($data as $id => $value)

                        //$listBoxGrandChilds .= CHtml::tag('option', array('value' => $id), CHtml::encode($value), true);

                        echo CHtml::tag('option', array('value' => $id), CHtml::encode($value), true);

                

                // return data (JSON)

                /*echo CJSON::encode(array(                        

                        'listBoxGrandChilds' => $listBoxGrandChilds

                ));*/

}



View




...

<div class="row">

		<?php echo $form->labelEx($item,'category_id'); ?>		

                <?php 

                        // Show the parents

                        echo $form->listBox($item, 'category_id', CHtml::listData(Category::model()->findAll('parent_id IS NULL'), 'id', 'name'),

                                array(                                        

                                        'ajax' => array(

                                                'type' => 'POST', // request type

                                                'url' => CController::createUrl('item/updatechild'), // url to call

                                                //'dataType' => 'JSON', // data type

                                                'data' => array('parent_id'=>'js:this.value'), // pass the id

                                                'update' => '#'.CHtml::activeId($item, 'category_lv2'), // selector to update

                                                /*'success'=>'function(data) {                                                                

                                                                $("#'.CHtml::activeId($item, "category_lv2").'").html(data.listBoxLv2);                                                                

                                                        }',*/

                                        )

                                )); 

                ?>

		<?php echo $form->error($item,'category_id'); ?>

	</div>                

        <div class="row"> 

                <?php echo $form->labelEx($item,'category_id'); ?>

                <?php 

                        // Show the childs 'linked' to the previous selected parent

                        echo $form->listBox($item, 'category_id', array(), array('id' => 'Item_category_lv2'),

                                array(                                           

                                        'ajax' => array(

                                                'type' => 'POST', // request type

                                                'url' => CController::createUrl('item/updategrandchild'), // url to call

                                                //'dataType' => 'JSON',

                                                'data' => array('child_id'=>'js:this.value'), // pass the id                 

                                                'update' => '#'.CHtml::activeId($item, 'category_lv3'), // selector to update

                                                /*'success' => 'function(data) {

                                                                $("#Item_category_lv3").html(data.dropDownCities);

                                                                $("#'.CHtml::activeId($item, "category_lv3").'").html(data.listBoxLv3);                                                                

                                                        };'*/

                                        )

                                ));                                                                                

                ?>

                <?php echo $form->error($item,'category_id'); ?>

        </div>

        

        <div class="row">

                <?php echo $form->labelEx($item,'category_id'); ?>

		<?php 

                        // Show the grandchilds 'linked' to the previous selected childs

                        echo $form->listBox($item, 'category_id', array(), array('id' => 'Item_category_lv3'));                        

                ?>		

                <?php echo $form->error($item,'category_id'); ?>

	</div>

...



With the above code, i managed to fill the 1st and 2nd listBox, but when i clicked on the 2nd nothing happens. Can someone help on this, or maybe give me the right way to archive this?

Here’s typically what I did for 3-level dependent dropdowns, I guess it should be the same logic?


<div class="row">

    <?php echo $form->labelEx($model,'parentId'); ?>

    <?php echo $form->dropDownList($model,'parentId',

                                   CHtml::listData(Parent::model()->findAll(array('order' => 'id')), 'id', 'parent'),

                                   array('empty' => 'Please select',

                                         'ajax' => array('type' => 'GET',

                                                           'url' => $this->createUrl('category/childs'),

                                                           'update' => '#' . CHtml::activeId($model, 'childId')

                                                        )

                                        )

                                   ); ?>

    <?php echo $form->error($model,'parentId'); ?>

</div>


<div class="row">

    <?php echo $form->labelEx($model,'childId'); ?>

    <?php

        if(isset($_POST['Category']['childId']))

            echo CHtml::dropDownList('Category[childId]', $_POST['Category']['childId'],

                                     CHtml::listData(Child::model()->findAll(array('condition' => 'parentId = :pId','params' => array(':pId' => $_POST['Category']['parentId']))), 'id', 'child'),

                                     array('empty' => 'Please select',

                                           'ajax' => array('type' => 'GET',

                                                           'url' => $this->createUrl('category/grandChilds'),

                                                           'update' => '#' . CHtml::activeId($model, 'grandChildId')

                                                        )

                                        )

                                    );

        else

            echo CHtml::dropDownList('Category[childId]', $model->childId,

                                     CHtml::listData(Child::model()->findAll(array('condition' => 'parentId = :pId','params' => array(':pId' => $model->parentId))), 'id', 'child'),

                                     array('empty' => 'Please select',

                                           'ajax' => array('type' => 'GET',

                                                           'url' => $this->createUrl('category/grandChilds'),

                                                           'update' => '#' . CHtml::activeId($model, 'grandChildId')

                                                        )

                                        )

                                    );

    ?>

    <?php echo $form->error($model,'childId'); ?>

</div>


<div class="row">

    <?php echo $form->labelEx($model,'grandChildId'); ?>

    <?php

        if(isset($_POST['Category']['grandChildId']))

            echo CHtml::dropDownList('Category[grandChildId]', $_POST['Category']['grandChildId'],

                                     CHtml::listData(GrandChild::model()->findAll(array('condition' => 'childId = :cId','params' => array(':cId' => $_POST['Category']['childId']))), 'id', 'grandChild'),

                                     array('empty' => 'Please select')

                                    );

        else 

            echo CHtml::dropDownList('Category[grandChildId]', $model->grandChildId,

                                     CHtml::listData(GrandChild::model()->findAll(array('condition' => 'childId = :cId','params' => array(':cId' => $model->childId))), 'id', 'grandChild'),

                                     array('empty' => 'Please select')

                                    );

    ?>

    <?php echo $form->error($model,'grandChildId'); ?>

</div>

Are you using 3 different models, i.e. 3 different tables (parent, child and grandchild) or are you using only one (category) like i describe ?

I posted only the view because our controllers look the same. Here’s mine anyway:


public function actionChilds()

{

    $data = Child::model()->findAll('parentId = :pId', array(':pId' => (int) $_GET['Category']['parentId']));

    $data = CHtml::listData($data, 'id', 'childName');

    if (count($data)) {

        echo CHtml::tag('option', array('value' => ''), CHtml::encode('Please select'), true);

        foreach($data as $value => $name) {

            echo CHtml::tag('option', array('value' => $value), CHtml::encode($name), true);

        }

    } else {

        echo CHtml::tag('option', array('value' => ''), CHtml::encode('Please select a parent first'), true);

    }

}


public function actionGrandChilds()

{

    $data = GrandChild::model()->findAll('childId = :cId', array(':cId' => (int) $_GET['Category']['childId']));

    $data = CHtml::listData($data, 'id', 'grandChildName');

    if (count($data)) {

        echo CHtml::tag('option', array('value' => ''), CHtml::encode('Please select'), true);

        foreach($data as $value => $name) {

            echo CHtml::tag('option', array('value' => $value), CHtml::encode($name), true);

        }

    } else {

        echo CHtml::tag('option', array('value' => ''), CHtml::encode('Please select a child first'), true);

    }

}

I have 3 models, but It’s the controller and the view of an independent one.

Here’s what I think: for you case, leave your Category table, controller and model as they are, and use CHtml::listBox() in order to be free with the input names/ids. So it’s just the view that would be changed by hardcoding input names/id like this:


// your model-linked syntax

echo $form->listBox($item, 'category_id', CHtml::listData(Category::model()->findAll('parent_id IS NULL'), 'id', 'name'),

                                array(                                        

                                        'ajax' => array(

                                                'type' => 'POST', // request type

                                                'url' => CController::createUrl('item/updatechild'), // url to call

                                                //'dataType' => 'JSON', // data type

                                                'data' => array('parent_id'=>'js:this.value'), // pass the id

                                                'update' => '#'.CHtml::activeId($item, 'category_lv2'), // selector to update

                                                /*'success'=>'function(data) {                                                                

                                                                $("#'.CHtml::activeId($item, "category_lv2").'").html(data.listBoxLv2);                                                                

                                                        }',*/

                                        )

                                )); 

// my suggested syntax

CHtml::listBox('Category[parentId]', array(), CHtml::listData(Category::model()->findAll('parent_id IS NULL'), 'id', 'name'),

                                array(                                        

…

                                                'update' => '#Category_childId', // hardcoded and not linked to your model

…



Well yeah, our controllers look almost the same :), but you uses different models (Child, Modele, …), so you have no problems with that since they have different id’s!

I got a solution by adding 2 more fields, but i dont like it. I’m going to look your code and try change something in mine!

Check my last suggestion using CHtml::listBox() instead of CActiveForm::listBox()

Edit: but that breaks the model validation - I’m stupid :D

Yeah that’s true, the “but that breaks the model validation” ofc :P

I already tryed that before and i remember that i had problems on validation/listBox interaction. That’s why i asked on the other thread if is possible validate the id defined by us.

In a more recent project where I had dependent div blocks, I “manually” validated my forms and it worked very well. Let me get back to see what I can suggest. Meanwhile if you solve your issue, keep us posted ;)

I think there can be multiple solutions, one of them is adding virtual attributes to your model (category_lv1 / category_lv2 / category_lv3) instead of forcing the listBoxes ids. It won’t break validation, and your ajax calls will work smoothly. (Don’t forget to add the virtual attributes to your safe array)

Thank you bennouna for all ideas and suggestions :), you really helped me in this issue, +1 for you. I changed my code to use the virtual attributes and you are totally right! The listBox/validation/ajax is working in the way i want.

Just one question, i really need define these virtual attributes as ‘safe’? Was not enough to define, e.g. ‘integerOnly’?




...

public function rules()

{		

	return array(

                ...

                array('category_id, parent, child, grandchild', 'numerical', 'integerOnly' => true),                        

                ...

		);

}

...



I’m glad you could achieve your goal without too much change :)

No, if you have already at least one validation rule, you’re right, you don’t need to define the attributes as safe.