Creating a dependent dropdown

44 followers

Often you'll need a form with two dropdowns, and one dropdown's values will be dependent on the value of the other dropdown. Using Yii's built-in AJAX functionality you can create such a dropdown.

The view with the form.

We'll show a form that shows countries and dependent of the country selected will show cities.

echo CHtml::dropDownList('country_id','', array(1=>'USA',2=>'France',3=>'Japan'),
array(
'ajax' => array(
'type'=>'POST', //request type
'url'=>CController::createUrl('currentController/dynamiccities'), //url to call.
//Style: CController::createUrl('currentController/methodToCall')
'update'=>'#city_id', //selector to update
//'data'=>'js:javascript statement' 
//leave out the data key to pass all form values through
))); 
 
//empty since it will be filled by the other dropdown
echo CHtml::dropDownList('city_id','', array());

The first dropdown is filled with several value/name pairs of countries. Whenever it is changed an ajax request will be done to the 'dynamiccities' action of the current controller. The result of that request (output of the 'dynamiccities' action) will be placed in the second dropdown with id is #city_id.

The controller action

It will have to output the html to fill the second dropdownlist. Furthermore it will do that dependent on the the value of the first dropdown.

public function actionDynamiccities()
{
//please enter current controller name because yii send multi dim array 
    $data=Location::model()->findAll('parent_id=:parent_id', 
                  array(':parent_id'=>(int) $_POST['Current-Controller']['country_id']));
 
    $data=CHtml::listData($data,'id','name');
    foreach($data as $value=>$name)
    {
        echo CHtml::tag('option',
                   array('value'=>$value),CHtml::encode($name),true);
    }
}

It will retrieve all cities that have as a parent the id of the first dropdown. It will then output all those cities using the tag and the output will end up in the second dropdown.

You might wonder where the $_POST['country_id'] comes from. It's simple, when the 'data' key of the ajax array in the first dropdown is empty, all values of the elements of the form the dropdown is in, will be passed to the controller via the ajax request. If you're using Firebug you can inspect this request and see for yourself.

This behaviour can also be changed. By default the value of the 'data' key in the ajax configuration array is js:jQuery(this).parents("form").serialize(). The preceding js: indicates to Yii that a javascript statement will follow and thus should not be escaped. So, if you change the 'data' key to something else preceded by 'js:' you can fill in your own statement. The same applies to the 'success' parameter.

For this to work you also need to edit the Method accessRules() (if available) in your current Controller. In this example we would change

array('allow', // allow authenticated user to perform 'create' and 'update' actions
                'actions'=>array('create','update'),
                'users'=>array('@'),
            ),

to

array('allow', // allow authenticated user to perform 'create' and 'update' actions
                'actions'=>array('create','update','dynamiccities'),
                'users'=>array('@'),
            ),

in order to allow access for authenticated Users to our Method 'dynamiccities'. To allow access to eg any User or to use more complex rules please read more about accessRules and Authentication here.

Note: For testing purposes you could also comment out

'accessControl', // perform access control for CRUD operations

in the filters() Method. This will disable all access controls for the controller. You should always re-enable it after testing.

Links

Chinese version

Total 9 comments

#4447 report it
re1nald0 at 2011/07/10 04:58am
Updating several dropdowns

Thank you for a nice tutorial! To update several dropdowns based on one dropdown value, we can use this:

In the view:

'ajax'=>array(
  'type'=>'POST', 
  'dataType'=>'json',
  'data'=>array('color'=>'js: $(this).val()'),
  'url'=>CController::createUrl('material/getGraniteOptions'),
  'success'=>'function(data) {
     $("#dropdownA").html(data.dropdownA);
     $("#dropdownB").html(data.dropdownB);
  }',
)

In the controller:

// fetch your data first
.....
 
foreach($dataA as $value=>$name)
   $dropDownA .= CHtml::tag('option', array('value'=>$value),CHtml::encode($name),true);
 
foreach($dataB as $value=>$name)
   $dropDownB .= CHtml::tag('option', array('value'=>$value),CHtml::encode($name),true);
 
// return data (JSON formatted)
echo CJSON::encode(array(
  'dropDownA'=>$dropDownA,
  'dropDownB'=>$dropDownB
));

Hope this may come handy to someone else.

#4197 report it
h3rm@n at 2011/06/15 11:15am
Not Run without form tag.

some time ago, I tried to practice the way of the above. but i can not get the post. in addition only that the $ _POST not be sent if not in the Form tag. and I added this code:

<?php $form=$this->beginWidget('CActiveForm', array(
    'id'=>'account-form',
    'enableAjaxValidation'=>true,
    'enableClientValidation'=>true,
    'focus'=>array($model1,'kdakun'),
 
)); ?>
 
<?
echo CHtml::dropDownList('country_id','', array(1=>'USA',2=>'France',3=>'Japan'),
array(
'ajax' => array(
'type'=>'POST', //request type
'url'=>CController::createUrl('gl/test'), //url to call.
//Style: CController::createUrl('currentController/methodToCall')
'update'=>'#city_id', //selector to update
//'data'=>'js:javascript statement' 
//leave out the data key to pass all form values through
))); 
 
//empty since it will be filled by the other dropdown
echo CHtml::dropDownList('city_id','', array());
?>
 
<?php $this->endWidget(); ?>

controller:

public function actionTest(){
                $tag = $_POST['country_id'];
 
                echo CHtml::tag('option',array('value'=>'A' ),CHtml::encode('A - ' .  $tag),true);
 
        }

hope may be useful.

#3942 report it
warden at 2011/05/23 07:33am
Default values

Just for the reference, to properly select 'selected' fields in dependent dropdown, I had to do as following:

<?php 
        echo CHtml::dropDownList('country_id',
        ($model->isNewRecord) ?  '' : $model->region->country->id, 
        CHtml::listData(Country::model()->findAll(), 'id', 'name'),
        array(
        'prompt' => '',
        'ajax' => array(
            'type'=>'GET', //request type
            'url'=> $this->createUrl('shop/ajaxregions'),  
            'update'=>'#Shop_region_id',
            'data'=>array('country_id'=>'js:this.value'),
        )));
        ?>
 
        <?php 
            echo $form->dropDownList($model, 'region_id', ($model->isNewRecord) ?  array() : CHtml::listData(CountryRegion::model()->findAll(), 'id', 'name'));
        ?>
#3621 report it
warden at 2011/04/24 09:52pm
city should be pre-filled

Actually, the city should not be empty as in the example:

echo CHtml::dropDownList('city_id','', array());

because when you are editing a record, it will not be filled in. If you need this to set to the actual data, it should be the standard way:

echo CHtml::dropDownList('city_id',$model->city_id, CHtml::listData(City::model()->findAll(),'id','name'));
#3620 report it
warden at 2011/04/24 05:00pm
no need to include controller

you don't need to pass controller name for the same controller:

'url'=>CController::createUrl('currentController/ajaxRegions'), //url to call.

enough is to write:

'url'=>CController::createUrl('ajaxRegions'), //url to call.
#2402 report it
andy_s at 2010/12/24 12:57am
A note about data param

By default, all form elements' values are sent. But we need only "country_id". So you can simply write:

'data'=>array('country_id'=>'js:this.value'),
#2213 report it
sasori at 2010/11/27 05:06am
here's the version that helped me, based from that example above

( well, mine is for a triple dependent drop down menu of countries, states, cities table )

This is for the Controller part,

public function actionDynamicstates()
    {
        $data = Worldareasstates::model()->findAll('CountryID=:parent_id',
                        array(':parent_id'=>(int) $_POST['Wsmembersdetails']['CountryID']));
 
 
        $data = CHtml::listData($data,'StateID','StateName');
            foreach($data as $id => $value)
            {
                echo CHtml::tag('option',array('value' => $id),CHtml::encode($value),true);
            }
 
    }
 
    public function actionDynamiccities()
    {
        $data = Worldareascities::model()->findAll('StateID=:parent_id',
                        array(':parent_id'=>(int) $_POST['Wsmembersdetails']['StateID']));
 
        $data = CHtml::listData($data,'CityID','CityName');
            foreach($data as $id => $value)
            {
                echo CHtml::tag('option',array('value' => $id),CHtml::encode($value),true);
            }
    }

here's the "_form" view code

<div class="row">
        <?php echo $form->labelEx($model,'Country'); ?>
        <?php 
              $country = new CDbCriteria; 
              $country->order = 'CountryName ASC';
        ?>
        <?php 
              echo $form->dropDownList($model,'CountryID',CHtml::listData(Worldareascountries::model()->findAll($country),'CountryID','CountryName'),
                        array(
                            'ajax' => array(
                            'type' => 'POST',
                            'url' => CController::createUrl('wsmembersdetails/dynamicstates'),
                            'update' => '#Wsmembersdetails_'.StateID
                        )       
                  )
              );
        ?>
        <?php echo $form->error($model,'CountryID'); ?>
    </div>
 
    <div class="row">
        <?php echo $form->labelEx($model,'State'); ?>
        <?php 
              $state = new CDbCriteria;
              $state->order = 'StateName ASC';
        ?>
        <?php 
              echo $form->dropDownList($model,'StateID',CHtml::listData(Worldareasstates::model()->findAll($state),'StateID','StateName'),
                        array(
                            'ajax' => array(
                            'type' => 'POST',
                            'url' => CController::createUrl('wsmembersdetails/dynamiccities'),
                            'update' => '#Wsmembersdetails_'.CityID
                        )   
                    )
              );
        ?>
        <?php echo $form->error($model,'StateID'); ?>
    </div>
 
 
    <div class="row">
 
        <?php echo $form->labelEx($model,'CityID'); ?>
        <?php echo $form->dropDownList($model,'CityID',array());?>
        <?php echo $form->error($model,'CityID'); ?>
    </div>

I hope that code snippet , can help anyone who'll be encountering same situation, yii yii yii :)

#2156 report it
KEo at 2010/11/19 08:59am
To form or not to form

For everyone how found difficulties in above tutorial I'd like to share my insights. Once you put both CHtml::dropDownList between <form> tags this example works like a charm. If not it will not work. The reason for that is that CHtml:ajax function use as default 'data' value the following statement:

js:jQuery(this).parents("form").serialize()

Means it looks for "form" parent.

Alternatively you can basically override the 'data' attribute with the following:

js:jQuery(this).serialize()

This cause the form is not necessary any more. The option value is taken directly from dropDownList element.

#486 report it
lgelfan at 2010/05/18 04:01am
Data attribute

Here is an example showing passing two values for the data element -- the first variable is named "language_id" and will pass the value dynamically (via JavaScript) of an HTML select element with an id of "language_id"; the second variable is named "foo" and will pass a static value of "bar":

'data'=>array('language_id'=>'js:$(\'#language_id\').val()', 'foo'=>'bar'),

You should be able to sort out what you need based on that. If you want to pass all the values in your form, just leave the data attribute blank, as in the example.

Leave a comment

Please to leave your comment.