Dynamic Select Box

Recently I came across a problem, where I wanted to show a select box for zones in an address (counties or states etc.). I have a list of zones in my database and to put them all in one box would have been unusable as the list is in excess of 2000 zones. My solution to this was only show the zones related to the country selected in another select box. When the user changes the country the zones are updated. In this tutorial I will explain how I achieved this using cakePHP (1.3) and the YUI Library (3.1.1).

If you want to try this, the following files have the sql to fill the two tables (this data is quite old, some some countries/states etc. may be out of date).
countries.sql world_zones.sql
The first thing to do is setup basic controller and model files for “zones” and “countries”, this could be done with the bake console. For this example I will use this to set to state/county in a profile, so to make use of the data in the zones and countries tables you need to add two fields, “zone_id” and “country_id”. Make sure associations are setup between your profiles model and these two new models (zones and countries). If you were going to show the full list of zones the profile/add action would look lie this:
function add() {
if (!empty($this->data)) {
$this->Profile->create();
if ($this->Profile->save($this->data)) {
$this->Session->setFlash(sprintf(__('The %s has been saved', true), 'profile'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(sprintf(__('The %s could not be saved. Please, try again.', true), 'profile'));
}
}
$zones = $this->Profile->Zone->find('list');
$countries = $this->Profile->Country->find('list');
$this->set(compact('zones', 'countries'));
}
Now as I said this would give you over 2000 zone options, which is not exactly practical. To reduce the list to only show the zones for one country all you have to do is add a condition to the zone find statement, as I live in the UK I would do this:
$zones = $this->Zone->find('list',array('conditions'=>'country_id = 222'));
Obviously this is great if you can guarantee that all your users are from one country. As this will almost never be the case the user needs to be able to select their country as well. This is an example of a basic form for our profile/add action.

<?php echo $this->Form->create('Profile');?>

Add Profile
<?php
echo $this->Form->input('name');
echo $this->Form->input('zone_id');
echo $this->Form->input('country_id',array('value'=>'222'));
?>

<?php echo $this->Form->end(__('Submit', true));?>

Now to be able to update the list of zones, a bit of JavaScript is required. The first thing to do is include the YUI seed file, ideally at the top of your layout file.

The following piece of code can be put in the profiles/add.ctp file or in it’s own file and linked from there.

YUI().use("node","io-base","event-focus",function(Y){
var d = Y.one('#zones');
var gH = {
write: function(str) {
d.set('innerHTML',str);
},
start: function() {
this.write("");
},
success: function(id,o) {
this.write(o.responseText);
},
failure: function() {
this.write("Error");
}
}
Y.on('io:start', gH.start, gH);
Y.on('io:success', gH.success, gH);
Y.on('io:failure', gH.failure, gH);

function call(e, b){
var country_id = e.currentTarget.get('value');
Y.log("Country id: " + country_id);
Y.io('http://' + location.hostname + '/zones/update/' + country_id);
}
Y.on('change', call, "#CountryId", this, false);
});

To break this down, the first thing this bit of code does is look for an element with an id of “zones”, then there is a function that sends an io request to http://www.example.com/zones/update/{country_id} and finally adds an on change event listener to an element with an id of “CountryId” (countries select box).
There are a few things that need to be changed for this code to work. The profiles/add.ctp file needs another div added and I add a short message to inform users how to access other zones and make sure there is an empty selection. Simply replace the “zone_id” input code with this:

<?php
echo $this->Form->input('zone_id',
array('empty'=>'',
'label'=>__('Area',true),
'after'=>__('Your area not listed? Try changing the country below.',true))
);
?>

The action that the io request needs is “update”, this needs to be added to the zones controller. You can also use this same function/code if you need zone and country selection in multiple places in an app.
function update($cid = null){
if(!empty($cid)){
$zones = $this->Zone->find('list',array('conditions'=>'country_id ='.$cid));
$this->set(compact('zones'));
}
}
This function needs a view file (zones/update.ctp) to return the updated zones list. This simply replicates the code in the profiles/add.ctp file except with the new data.
<?php
if(isset($zones)){
echo $this->Form->input('zone_id',
array('empty'=>'',
'label'=>__('Area',true),
'after'=>__('Your area not listed? Try changing the country below.',true))
);
}else{
__('Error');
}
?>
When the io request is started the zones div is replaced by an image called bar_loader.gif, you can change this to the name of a loading gif you already have or download one.
To review, there are some things you need to make sure you do before this will work;

  • Zone, Country and Profile models need to be set up with associations.
  • Profiles controller add action needs to have zones limited to one country.
  • Profiles add view file needs JavaScript added, including YUI seed file.
  • Zones controller needs update action added.
  • Zones update view file needs to be created.

I hope to create some demos of the tutorials I have done so check back soon.
I hope you have liked this tutorial, please feel free to leave a comment or retweet below.