Is there a way to make dependent drop down menus on forms that use standard yii validatation?

I have been hitting my head against this problem all night.

What I need is a simple multiple selection list box that is dependent on a drop down list. But there are some other fields on the form that need to be validated. I have the whole thing working just fine until it is time to validate (or re-render for any other reason).

The controlling drop down list and all the other fields display very nicely along with explanatory messages when I use yii’s standard validation. However, the ajax routine that populates the dependent list box is not called unless a user manually changes the controlling drop down list, and thus both the controlling drop down field and dependent multiple list box fields must be reselected by the user every time there is any validation error in any of the other fields.

Is there any way to invoke the ajax routine that propagates the dependent list box whenever there are any validation errors without user intervention?

Here is my view code:


$form=$this->beginWidget('CActiveForm', array(

	'id'=>'working-block-model-element-display-WorkingBlockModelElement-form',

	'enableAjaxValidation'=>false,

)); ?>


<?php echo $form->errorSummary($model);?>


<div>

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

<?php $kmodel = WorkingBlock::model()->findAll(array('order' => 'name'));

 echo $form->dropDownList($model, 'working_block', CHtml::listData($kmodel, 'id', 'name'),array('prompt'=>'please select...',

'ajax' => array(

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

'url'=>CController::createUrl('Display/dynamicwbmwbe'), //url to call.

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

))); ?>

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

</div>


<div class="row">

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

<?php echo $form->listBox($model,'working_block_model_elements',$kjk,$htmlOptions=array('multiple'=>true,'size'=><img src='http://www.yiiframework.com/forum/public/style_emoticons/default/cool.gif' class='bbc_emoticon' alt='8)' />);

?>

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

</div>


... more fields that need to be validated ...


<div class="row buttons">

<?php echo CHtml::submitButton('Submit'); ?>

</div>


<?php $this->endWidget(); ?>

Here is my controller ajax routine:


public function actionDynamicwbmwbe()

{  

    $kj=$_SERVER['HTTP_REFERER'];

    $kp="";

    $where=strrpos($kj,'/');

    if ($where!==NULL)

        $kp=substr($kj,$where+1);

    $kp.='Display';

    $kk=$_POST[$kp]['working_block'];


     $connection = Yii::app()->db;


$sql3='SELECT DISTINCT weather_station.id, weather_station.name, weather_element.id AS id2, weather_element.abbrev, weather_element.units FROM weather_element, working_block_model, working_block_model_input_element, weather_station WHERE working_block_model.working_block_id = '.$kk.' ';

$sql3.='AND working_block_model_input_element.weather_element_id = weather_element.id AND working_block_model.id = working_block_model_input_element.working_block_model_id AND weather_station.id = working_block_model_input_element.weather_station_id ';

$sql3.='ORDER BY weather_station.name ASC, weather_element.abbrev ASC;';


$rows = $connection->createCommand($sql3)->queryall();

foreach ($rows as $row)

{

    $data[$arrcnt]['id']='2,'.$row['id'].','.$row['id2'].',';

    $data[$arrcnt]['name']=$row['name'].'-'.$row['abbrev'];

    if ($row['units'] != "")

    {

        $data[$arrcnt]['name'].='-'.$row['units'];

    }

    $data[$arrcnt]['id'].=$data[$arrcnt]['name'];

    $arrcnt++;

}


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

    

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

    {

        echo CHtml::tag('option',

                   array('value'=>$value),$name,true);

    }

}



Any help or insight would be greatly appreciated! Both yii and jquery are new to me, so I am really floundering here. Thanks so much!

When the form is rendered during validation you have correct value in the $model->working_block, because drop down list is displayed properly, right?

So you need to set $model->working_block_model_elements accoringly to value of $model->working_block to get correct initial selection for list box - ActiveForm methods like listBox recognize model’s value to display initial value on the form.

So you think I should be doing a bunch of server calls in my view as well as my controller and duplicating code in both both places so I can recreate the selection criteria, then propagate the listBox with the same data the controller routine just propogated it with, then match these choices against those contained in the _POST variable in order to highlight the choices the user just made?

Sure, I thought of that. But it be far easier and better if there were some way to call the same ajax routine to do what it just did before. Why can the ajax routine only be triggered when the user changes his or her selection? The time yii saved me with the initial display and validation routines is nice, but putting the exact same logic (and more) in the view that is in the controller is against my religion. ;D

Here is the entire ajax routine, so you can see what I mean:


public function actionDynamicwbmwbe()

{  

    $kj=$_SERVER['HTTP_REFERER'];

    $kp="";

    $where=strrpos($kj,'/');

    if ($where!==NULL)

        $kp=substr($kj,$where+1);

    $kp.='Display';

    $kk=$_POST[$kp]['working_block'];


     $connection = Yii::app()->db;


$sql1='SELECT DISTINCT working_block_element.id, spatial_summary_level.id AS id2, working_block_element.abbrev, spatial_summary_level.abbrev AS abbrev2, working_block_element_type.units FROM working_block_element, working_block_model, spatial_summary_level, working_block_element_type WHERE ';

$sql1.='working_block_model.working_block_id = '.$kk.' AND ';

$sql1.='working_block_model.working_block_element_id = working_block_element.id AND spatial_summary_level.id = working_block_model.spatial_summary_level_id AND working_block_element_type.id = working_block_element.working_block_element_type_id ORDER BY working_block_element.abbrev asc, spatial_summary_level.abbrev asc, working_block_element_type.units asc;';


$sql2='SELECT DISTINCT working_block_element.id, spatial_summary_level.id AS id2, working_block_element.abbrev, spatial_summary_level.abbrev AS abbrev2, working_block_element_type.units, weather_station_id as id3, weather_element_id AS id4 FROM working_block_element, working_block_model, working_block_model_input_element, spatial_summary_level, working_block_element_type WHERE ';

$sql2.='working_block_model.working_block_id = '.$kk.' AND working_block_model_input_element.working_block_element_id = working_block_element.id ';

$sql2.='AND working_block_model.id = working_block_model_input_element.working_block_model_id AND spatial_summary_level.id = working_block_model_input_element.spatial_summary_level_id AND working_block_element_type.id = working_block_element.working_block_element_type_id ORDER BY working_block_element.abbrev asc, spatial_summary_level.abbrev asc, working_block_element_type.units asc;';


$sql3='SELECT DISTINCT weather_station.id, weather_station.name, weather_element.id AS id2, weather_element.abbrev, weather_element.units FROM weather_element, working_block_model, working_block_model_input_element, weather_station WHERE working_block_model.working_block_id = '.$kk.' ';

$sql3.='AND working_block_model_input_element.weather_element_id = weather_element.id AND working_block_model.id = working_block_model_input_element.working_block_model_id AND weather_station.id = working_block_model_input_element.weather_station_id ';

$sql3.='ORDER BY weather_station.name ASC, weather_element.abbrev ASC;';


$sql4='SELECT orientation_pair.side1_abbrev, orientation_pair.side2_abbrev FROM working_block, orientation_pair WHERE working_block.id='.$kk.' and working_block.orientation_pair_id=orientation_pair.id';

$sides = $connection->createCommand($sql4)->queryall();


$rows = $connection->createCommand($sql1)->queryall();

$arrcnt=0;


foreach ($rows as $row)

{

    $data[$arrcnt]['id']='1,'.$row['id'].','.$row['id2'].',';

    $sql5 = 'SELECT side from horizontal_position, spatial_summary_level where spatial_summary_level.id = '.$row['id2'].' AND horizontal_position.id = spatial_summary_level.horizontal_position_id';

    $pp=11;

    $horizontal = $connection->createCommand($sql5)->queryRow();

    $jj=$horizontal;

    if ($row['abbrev2'] != "")

    {

        $data[$arrcnt]['name']=$row['abbrev'].'-'.$row['abbrev2'];

    }

    else

    {

        $data[$arrcnt]['name']=$row['abbrev'];

    }

    if ($horizontal['side'] == 1)

        $data[$arrcnt]['name'].='-'.$sides[0]['side1_abbrev'];

    elseif ($horizontal['side'] == 2)

        $data[$arrcnt]['name'].='-'.$sides[0]['side2_abbrev'];

    if ($row['units'] != "")

    {

        $data[$arrcnt]['name'].='-'.$row['units'];

    }

 //   $data[$arrcnt]['name']=html_entity_decode($data[$arrcnt]['name']);

    $data[$arrcnt]['id'].=$data[$arrcnt]['name'];

    $arrcnt++;

}


$kk=22;

$rows = $connection->createCommand($sql2)->queryall();

foreach ($rows as $row)

{

    if ($row['id3']==0 || $row['id4']==0)

        $data[$arrcnt]['id']='1,'.$row['id'].','.$row['id2'].',';

    else

        $data[$arrcnt]['id']='3,'.$row['id'].','.$row['id2'].','.$row['id3'].','.$row['id4'].',';

    $sql5 = 'SELECT side from horizontal_position, spatial_summary_level where spatial_summary_level.id = '.$row['id2'].' AND horizontal_position.id = spatial_summary_level.horizontal_position_id';

    $pp=11;

    $horizontal = $connection->createCommand($sql5)->queryRow();

    $jj=$horizontal;

    if ($row['abbrev2'] != "")

    {

        $data[$arrcnt]['name']=$row['abbrev'].'-'.$row['abbrev2'];

    }

    else

    {

        $data[$arrcnt]['name']=$row['abbrev'];

    }

    if ($horizontal['side'] == 1)

        $data[$arrcnt]['name'].='-'.$sides[0]['side1_abbrev'];

    elseif ($horizontal['side'] == 2)

        $data[$arrcnt]['name'].='-'.$sides[0]['side2_abbrev'];

    if ($row['units'] != "")

    {

        $data[$arrcnt]['name'].='-'.$row['units'];

    }

  //  $data[$arrcnt]['name']=html_entity_decode($data[$arrcnt]['name']);

    $data[$arrcnt]['id'].=$data[$arrcnt]['name'];

    $arrcnt++;

}


$rows = $connection->createCommand($sql3)->queryall();

$jj=11;

foreach ($rows as $row)

{

    $data[$arrcnt]['id']='2,'.$row['id'].','.$row['id2'].',';

    $data[$arrcnt]['name']=$row['name'].'-'.$row['abbrev'];

    if ($row['units'] != "")

    {

        $data[$arrcnt]['name'].='-'.$row['units'];

    }

   // $data[$arrcnt]['name']=html_entity_decode($data[$arrcnt]['name']);

    $data[$arrcnt]['id'].=$data[$arrcnt]['name'];

    $arrcnt++;

}


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


   $jj=12;


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

    {

        echo CHtml::tag('option',

                   array('value'=>$value),$name,true);

    }


}




}




I do not suggest to put the same code to the view.

You can move most part of your ajax routine to the separate method. And call this method both in the ajax action and in the view (or better - in the controller action before rendering view).

This will not increase server calls - view is generated on the serever during the single request.

So your method can look like:




public function findElementsByBlock($blockId)

{  

     $kk=$blockId;


     $connection = Yii::app()->db;

     ...


     return $connection->createCommand($sql3)->queryall();

}


public function actionDynamicwbmwbe() {

    ...

    $rows = findElementsByBlock($_POST[$kp]['working_block']);

    ...

   

}


// action where validation is done

public function actionUpadte() {  


    ...

    

    $model->working_block_model_elements = findElementsByBlock($model->working_block); 

    $this->render('view', array('model'=>$model, ...);


}



Other way - is to write JS code which will execute on page load the same ajax request as now executed on drop down change. But this method will make additional server request. For this method you need to add something like this in you view:

[code]

&#036;script = &quot;&#036;('#WorkingBlockModelElement_working_block&quot;).trigger('change');


Yii::app()-&gt;clientScript-&gt;registerScript('tablesorter', &#036;script, CClientScript::POS_READY);

[code]

OK, that’s good advice! Thanks!

I understand everything you posted except this part:


public function actionUpdate() {  


    ...

    

    $model->working_block_model_elements = findElementsByBlock($model->working_block); 

    $this->render('view', array('model'=>$model, ...);


}

I guess I’m not very clear on how rendering a view a works. Before the code was called as part an active form request and as such it was populated with:


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

    {

        echo CHtml::tag('option',

                   array('value'=>$value),$name,true);

    }



How do I populate it in the controller action? Directly to the model array? If so, how do I tell the model array which of the potential choices was selected the last time around?

Thanks again!

Inside view you now using CActiveForm widget. Each time you call method like $form->dropDownList this call is redirected to CHtml::activeXXX method (CHtml::activeDropDownList in this case).

You can check how it is implemented, but general idea is that method takes model and field name as argument and outputs HTML element for it where value is set to the current $model->$field_name value.

That is why you have correct value in the drop down list after form validation.

So I think the problem is that when you call $form->listBox($model,‘working_block_model_elements’, …) then value for this field is not set and not displayed.

What I suggested to try is to set value for this field before rendering form. After that it should be displayed correctly.

In this case you do not need to convert array of ids to the HTML options, because you are working directly with the model in this case.

You need that "foreach" only in the action for ajax request, because it returns this data as HTML to the browser where this HTML is inserted to the list box.

By the way there is CHtml::listOptions method which you can use instead of foreach.

Thanks a lot! You were a big help to me. Once I learn more, I hope to return the favor someday.