Dynamic, asynchronous country and state dropdowns

You’ve seen it in action. You’re filling out a form, and there’s a dropdown list of countries. Once you select a country, a second dropdown shows a list of states or provinces for that country. See the demo here.

It’s pretty cool, and a great timesaver. But how do you do it?

Building it takes three things:

  1. A database of countries and their states/provinces.
  2. Scripts to build the dropdowns
  3. A way to trigger the construction of the state/province dropdown once a selection is made in the country dropdown

Let’s go through those one by one. This example uses jQuery and PHP, but you can use any scripting languages you want.

Database of country and state names

There are several ways to get the country/state data into your script, from hardcoding them into an array in your script to maintaining an external database.

But that’s a pain. Over time, countries appear, disappear, change names, etc. If you keep any sort of local list, you either have to maintain it, or accept that your list will slowly get outdated.

A better approach is to use an external data source that is automatically updated whenever there is political change around the world.

The best open-source database for that purpose is Geonames.org. It tracks more than 8 million pieces of geographical data.

They have an API for hitting their data directly, allowing you to slice and dice your queries. But the API has two disadvantages:

  1. Learning curve;
  2. Support issues. The PHP interface, for example, is a PEAR package that isn’t supported by most major hosting services.

Luckily, GeoNames also does automatic daily updates of some XML lists. We can grab those lists and parse them for the data we want, bypassing the API entirely.

Scripts to build the dropdowns

You’ll need two PHP scripts — one for each dropdown.

Country dropdown

This code grabs the “countryInfo” XML file and loops through it, pulling out the name and ID of each country in the list and writing them as options inside a <select> tag. The code can be written directly into your form.

<?php
//Build countries dropdown from GeoNames database
$xmlcountries = 'http://ws.geonames.org/countryInfo';
echo '<select name="custom_country" id="custom_country">';
$countries = simplexml_load_file($xmlcountries);
echo '<option value="">Your country</option>';
foreach ($countries->country as $country) {
    echo '<option value="'.$country->geonameId.'|'.$country->countryName.'">'.$country->countryName.'</option>';
}
echo '</select>';
?>

A couple of things to notice:

  1. We’re using a GeoNames XML file called “countryInfo”. it’s automatically updated every day.
  2. In each <option> tag’s “value” attribute, we’re writing both the country name and its geonameID, separated by a “|”. The country name is for humans; the geonameID is necessary to fetch the list of states/provinces associated with each country.

State dropdown

This script finds all the first-level subdivisions (states/provinces) of the country selected in the country dropdown and writes them as options for a <select> tag. It will be an external script, so let’s call it “state_select.php”.

<?php
$geonameid = explode('|',$_POST['id']);
$geonameid = $geonameid[0];

//Only build state select if country is USA or Canada
/*if($geonameid == '6252001' || $geonameid == '6251999') {*/    ?>

<select id="custom_state" name="custom_state">
    <option value="">Choose your state/province:</option>
<?php
        $stateurl = "http://ws.geonames.org/children?geonameId=" . $geonameid;
        $statexml = simplexml_load_file($stateurl);
            foreach ($statexml->geoname as $link)  {
                $statename = $link->name;
                echo '<option value="' . $statename . '">' . $statename . '</option>' . "\n";
            } ?>
</select>
<?php //} ?>

Things to note:

  1. We’re using the GeoNames XML file called “children”, and passing it the geonamesID of the selected country;
  2. To get the geonames ID, we explode the $id string into an array using the “|” as our separator. The first value in the array ($geonameid[0]) is the ID number, so that’s what we use. If we wanted the country name, we’d use $geonameid[1].  We’ll do that later on when it’s time to send the submitted form to a human.
  3. I’ve included an optional, commented-out conditional wrapper in case you want to show state/provinces for only a few countries. Here in the United States, you rarely care what province someone in Albania comes from. You save your users a step and improve your form submission rate by only asking for the state/province when you really need it.

Trigger the construction of the state dropdown

Finally, back in the main page, we’ll use jQuery to asynchronously trigger the “select_state.php” script, based on the technique outlined here.

HTML

<div id="state-select"></div>

jQuery

<script type="text/javascript">
jQuery("select#custom_country").change(function(){
    var $id = jQuery("select#custom_country option:selected").attr('value');
    jQuery.post("select_state.php", {id:$id}, function(data){
        jQuery("div#state-select").html(data);
        });
    });
</script>

Full details on this script are in the link above, but in a nutshell here’s what it does:

  1. Detects that a value has been selected in the “#custom_country” dropdown;
  2. Puts the chosen value into a variable;
  3. Passes that variable to the “select_state.php” script;
  4. Takes the output from “select_state.php” and puts it inside the “#state-select” div.

Parsing the country/state data on submit

When the form is submitted, the country/state data will look something like this:

$custom_country: "6252001|United States"
$custom_state: "Minnesota"

In your form processing script, you can use the following code to ditch the country ID number and send only the country name:

$country = explode('|',$_POST['custom_country']);
$country = $country[1];

Summary

That’s it. Put all the above pieces together, and here’s what you have:

  1. A country dropdown populated automatically from the GeoNames XML file;
  2. When a country is chosen from the dropdown, another dropdown appears, containing that country’s states/provinces — populated automatically from another GeoNames XML file;
  3. An optional wrapper in the code that generates the state dropdown lets you limit the countries that require a state/province selection.

 Update: shared server issues

If your site is hosted on a shared server, your hosting company may have disallowed external URLs for simplexml_load_file(), meaning the above code won’t work. To get around that add the following functions to your script, courtesy of Michael Morrison:

<?php
function load_file_from_url($url) {
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_REFERER, 'http://yourwebsite.com');
    $str = curl_exec($curl);
    curl_close($curl);
    return $str;
  }
  function load_xml_from_url($url) {
    return simplexml_load_string(load_file_from_url($url));
  }
?>

The functions use PHP’s cURL library to fetch the file, getting around the restrictions placed on simplexml_load_file(). Once you’ve added the functions, you just need to do two things:

  1. Replace “yourwebsite.com” in the above sample with the name of your website;
  2. Wherever my code says to use simplexml_load_file(), use load_xml_from_url() instead.

 

 

15 comments on “Dynamic, asynchronous country and state dropdowns

  1. Pingback: Using jQuery to run a server-side script

  2. Nahum on said:

    cool man this is working for me thanks… ;)

    • Michael on said:

      The script works when I use echo, but when I try and place the country drop down section into my $display_block, it won’t display the state drop down list.
      I generate all my content html in a $display_block then print it in the body of the html.
      I’ve tried using 2 display blocks, but that still doesn’t work.
      Using echo places the country drop down outside of my display block, which messes up all my CSS. Using echo also stops me from using a redirect header(“location: *** “)

      What could be wrong, and what can I do to fix it?

      ***** MY CODE *****
      $display_block .= “My previous code here”;

      //Build countries dropdown from GeoNames database
      $xmlcountries = ‘http://ws.geonames.org/countryInfo';
      $display_block .= ” “;
      $countries = simplexml_load_file($xmlcountries);
      $display_block .= “Your country”;

      foreach ($countries->country as $country) {
      $display_block .= “geonameId.”|”.$country->countryName.”\”>”.$country->countryName.””;
      }
      $display_block .= ”

      “;

      • Steven Ray on said:

        @Michael,

        Your code sample is incomplete. Possibly the comment box stripped out some tags. Try putting your code inside code blocks (<code></code>).

  3. Michael on said:

    WTF?!

    $xmlcountries = ‘http://ws.geonames.org/countryInfo';
    $countries = simplexml_load_file($xmlcountries);

    $display_block .=


    Your country

    foreach ($countries->country as $country) {

    $display_block .=
    geonameId."|".$country->countryName."\">“.$country->countryName.”

    }

    $display_block .=

    • Michael on said:

      ???
      Everything is the same as with your example, except the echo’s have been replaced by $display_block .= and then the “s have been escaped by using /”

      The country drop down displays, but it doesn’t create the state drop down…

      • Steven Ray on said:

        Hmm. I built a version of this using your way, but without escaping the quotes. It works.

        The code I used (turns out that in these comment boxes you have to use character encoding for the “>” and “<” characters):


        <?php
        //Build countries dropdown from GeoNames database
        $xmlcountries = 'http://ws.geonames.org/countryInfo';
        $display_name .= '<select name="custom_country" id="custom_country">';
        $countries = load_xml_from_url($xmlcountries);
        $display_name .= '<option value="">Your country</option>';
        foreach ($countries->country as $country) {
        $display_name .= '<option value="'.$country->geonameId.'|'.$country->countryName.'">'.$country->countryName.'</option>';
        }
        $display_name .= '</select>';
        echo $display_name;
        ?>

        Some things that might cause the problem:

        1. Remembering to echo the result;
        2. Making sure the “#state-select” div is available in the DOM when jQuery needs to write to it.

        • Michael on said:

          Hi Steven,

          It works when I echo $display_name in the php before the html section, but once again it messes up the displaying.
          When I move it to the html at the bottom of my code, it doesn’t work…

          html
          scripts
          body
          php echo display_block1 the first part of my code
          php echo display_name display drop down
          php echo display_block2 the last part of my code

          or
          body
          php echo display_block1
          your original code
          div select-state
          php echo display_block2

          I normally just place everything in one display_block and echo it in the body. Its been working well for everything else.
          I’ve tried swapping them around, and placing everything in the html.

          What am I not getting?
          Could it be the DOM? How would I fix it?

          Thank you for the help so far, I greatly appreciate your time.

          • The Geonames web service is free, and as such is bandwidth limited. If you’re not getting results, it could be (as has happened to me) that the service is currently being overloaded.
            I ended up saving the xml files locally each time a state is called, and unless it exists locally then pull from Geonames.

  4. AP Dubey on said:

    Thanks a lot for this

    i posted my query too that it wasnt workin earlier but i figured out the solution all i had to do it add function document.ready in jquery and once i added that i worked like charm.

    just one more help is it possible if i can get city list too from state list

    thanks in advance
    AP Dubey

    • Steven Ray on said:

      Everything in Geonames has its own ID, so you can find child elements by grabbing the state’s ID and using it to filter the “children” list.

      For instance, you’d run a United States filter using the ID 6252001:
      http://ws.geonames.org/children?geonameId=6252001

      If you look at the XML of that result, you’ll see the first state is Alabama. It has an ID of 4829764.

      If you query the “children” list using that ID, you’ll get a list of the counties in Alabama:
      http://ws.geonames.org/children?geonameId=4829764

      If you take a county and use it’s ID, you’ll get a list of cities in that county. For instance, Autauga County has the ID of 4830868. So if we run that query:
      http://ws.geonames.org/children?geonameId=4830868

      You’ll get a list of cities, starting with Autaugaville.

      I’ve never attempted to jump straight from state to city, so I don’t know if there’s a shortcut outside of the API. At a minimum, you could find all the cities in the state by looping through all the counties in the state.

  5. Steve,

    Working with the script, I’ve got it up and running, but trying to figure out one thing.

    You have this code:
    $country = explode(‘|’,$_POST[‘custom_country’]);
    $country = $country[1];

    Which I’d like to implement, but can’t figure out where since I’m using a salesforce Web2Lead form submission. So the processing page is on the SF side.

    • Steven Ray on said:

      Without access to the processing page, you can’t use the server-side code option.

      What you could do is modify the jQuery to add a hidden field containing only the chosen country name. You’d take the value of the chosen country, parse it for the country name, and then add a hidden field with that country name as the value, something like this:

      <input type=”hidden” name=”country_name” id=”country_name” value=”jquery_written_country_name” />

  6. Wailintun on said:

    thanks a lot! this web site made me perfect
    I have never seen this before!

  7. The state dropdown is not coming up for me. Here is my code:

    0){
    echo ”;
    foreach($errors as $e){
    echo ” . $e . ”;
    }
    echo ”;
    }
    ?>
    School:LakesClover Park
    First Name*
    Last Name*
    Maiden Name
    Mailing Address
    City
    <?php
    //Build countries dropdown from GeoNames database
    $xmlcountries = 'http://ws.geonames.org/countryInfo&#039;;
    echo '';
    $countries = simplexml_load_file($xmlcountries);
    echo ‘Your country';
    foreach ($countries->country as $country) {
    echo ‘geonameId.’|’.$country->countryName.'”>’.$country->countryName.”;
    }
    echo ”;
    ?>
    State/Province

    jQuery(“select#custom_country”).change(function(){
    var $id = jQuery(“select#custom_country option:selected”).attr(‘value’);
    jQuery.post(“select_state.php”, {id:$id}, function(data){
    jQuery(“div#state-select”).html(data);
    });
    });

    Username*
    Email*
    Password*
    Confirm Password*

Leave a Reply

Your email address will not be published. Required fields are marked *

*

* Copy This Password *

* Type Or Paste Password Here *

108,324 Spam Comments Blocked so far by Spam Free Wordpress

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>