There are situations when the form should show two select boxes in the parent - child style. For example when in the parent select box user selects country the child select should show cities only of that country.
There are at least two approaches to update child select box:
- load all cities and update contents of child select box by javascript filter;
- load cities from server on each change of country.
While the first one is simple it could be not the optimal solution for big arrays of data. Current tutorial will show how to make the second one with CapePHP.
The solution is to send request to server on each parent box selection change and receive json data to update the contents of dependent select box.
The server side
To handle request on server side we will create controller action. To make it more reusable we will split our action between the base AppControler and model specific controller. Here is the AppController common part /app/controllers/app_controller.php (part):
/** * Outputs model >find list in json format. * Used for dependent select to update data after parent select changed, */ function jsonList($model, $conditions) { if (!$conditions) { $mixes = array(); } else{ $mixes = $model->find('list', $conditions); } $json = array(); $i = 0; foreach ($mixes as $key => $mix) { $json[$i]['id'] = $key; $json[$i]['name'] = $mix; $i++; } $this->set('jsonVariables', $json); $this->render(null, 'ajax', '/elements/json'); exit(); }
We also need to setup clean layout for our requests. Here is the /app/views/element/json.ctp:
<?php echo $javascript->object($jsonVariables); ?>
And the custom invocation part could look like this /app/controllers/cities_controller.php (part):
function listCities($id = null) { $this->jsonList( $this->Mymodel->City, $id ? array('conditions' => array('country_id' => $id)) : null); }
Do not forget to filter child select box data in your controller /app/controllers/cities_controller.php (part):
$positions = $this->Experience->Position->find('list', array('country_id' => $this->data['Mymodel']['country_id']));
That all on server side.
The client side
After ‘make bake’ you will get form with independent select boxes like:
echo $form->input('country_id', array('empty' => true)); echo $form->input('city_id', array('empty' => __('Some text for empty element', true)))
Leave them as is. We will attach javascript to them. To reduce amount of code to write for each dependent select box I’ve created /app/views/elements/jsonselect.ctp:
<script type="text/javascript"> //<![CDATA[ $(document).ready(function() { $('<?=$parent?>').change(function(){ $.getJSON('<?=$source?>' + $(this).val(), function(data) { $('<?=$child?>').empty(); <? if (isset($empty) && $empty != false) : ?> $('<?=$child?>').append("<option value=\"\"><?= (is_bool($empty) ? ' ' : $empty) ?></option>"); <? endif ?> $.each(data, function(optionIndex, option) { var html = "<option value=\"" + option['id'] + "\">" + option['name'] + "</option>"; $('<?=$child?>').append(html); }) }) }); $('<?=$parent?>').keyup(function() {$('<?=$parent?>').change();}); }); //]]> </script>
To use the element place the following code in the end of your view, (e.g. /add.ctp):
<?php echo $this->renderElement('jsonselect', array( 'parent'=>'#MymodelCountryId', 'child'=>'#MymodelCityId', 'source'=>'listCities/', 'empty' => __('Some text for empty element', true))); ?>
If you dont want empty element set ‘empty’ => false or just ignore the ‘empty’ parameter.
That’s all.
April 18th, 2008 at 7:47 am
Hi mate!
Nice article, it worked like a charm!
I have just one concern: how could you add a dummy option like “Please select…” to the parent select? Because if the user gets his option selected by default, the select input never changes then the child select never changes, and that is a bit confusing to the user.
Thanks a lot!
April 18th, 2008 at 9:02 am
I’ve updated the post to include empty=’true’ parameter to the $form->input function. Now both select boxes will be empty by default.
April 19th, 2008 at 8:44 pm
Cheers mate, it’s a great tutorial!
July 7th, 2008 at 11:33 pm
I have to download script.aculo.us?,I did it and I putted it at app/webroot/js folder, but it seems isnt working.
July 8th, 2008 at 10:16 am
Example requires jquery-1.2.3.js.
June 28th, 2009 at 6:45 am
Hi! I don’t understand what this sentence does could someone be so kind to explain it to me? and im having trouble making this work, maybe is because this was coded with an older version of cake? (besides my lack of understanding of ajax)
$positions = $this->Experience->Position->find(’list’,
array(’country_id’ => $this->data[’Mymodel’][’country_id’]));
Thanks in advance, and thanks for writing this tutorial!
July 1st, 2009 at 12:09 am
The find ‘list’ will return array with id and label values to populate html select box. Reffer to the Model::find method source code for details and look for ‘list’ keyword.