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:

  1. load all cities and update contents of child select box by javascript filter;
  2. 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) ? '&nbsp;' : $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.

Posted by Rostislav Palivoda, filed under PHP. Date: April 2, 2008, 8:03 pm |

7 Responses

  1. Paolo Says:

    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!

  2. Rostislav Palivoda Says:

    I’ve updated the post to include empty=’true’ parameter to the $form->input function. Now both select boxes will be empty by default.

  3. Paolo Says:

    Cheers mate, it’s a great tutorial!

  4. joaquin Says:

    I have to download script.aculo.us?,I did it and I putted it at app/webroot/js folder, but it seems isnt working.

  5. Rostislav Palivoda Says:

    Example requires jquery-1.2.3.js.

  6. Simon Says:

    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!

  7. Rostislav Palivoda Says:

    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.

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.