More Cascading AJAX Dropdowns with CodeIgniter

ci_logo

Probably the most popular post in the history of this blog is “Populate DropDowns with jQuery and Codeigniter”. In that post, I gave a quick overview of how to set up a dropdown or select box that depends on the value selected in another form element. That post was never really intended to be a complete “How To”, but it’s been such a popular post that I wanted to revisit the idea, and write up a more complete discussion of this technique.

What I’m going to do here is to create and populate an HTML select element using values that are dependent on a selection in another HTML input. In this case, I’m using two select elements, but you could adapt the technique for any kind of input. For example, let’s say you have a form that displays data for a zip code. Now, there are a LOT of zip codes in the US, and you probably don’t want to make your users scroll through such a long list to choose the value they’re interested in. So, you add another HTML select element that allows the user to choose the specific state that contains the zip code. So, initially, the zip code select element is blank, or set to some default value. Then once the user chooses a state, the zip code element is populated with all of the zip codes for that state. Nice, huh?

Now, granted; this is a very specific use case. Don’t get hung up on this one example. As the perl mongers say, “There’s more than one way to do it”. What I’m trying to do here, is to show the technique. This code is not production ready. Also, the earlier post used jQuery to handle the population of the select element. In this example, I’m using straight javascript to set up the dropdown element. That way, you can adapt the technique to your javascript library of choice, whether it be MooTools, or Prototype, or whatever…

I’ve set up a working demo of the Cascading Dropdowns at the TechnoPoetic CodeIgniter Lab , so you can see it in action. It’s a simple, but powerful technique that can be adapted for use in lots of different situations.  Here’s how it’s done…

The controller has an action (or method) that gathers the initial data. In this case, it gets the list of state abbreviations from the database. It then renders the view that shows the HTML form.  The view contains some javascript that fires an AJAX request at the “onchange” event of the first dropdown. So, when the user chooses a state, the request is fired.  The controller has a second action that responds to the AJAX request. This method receives the value for the state that the user has chosen, and uses that to query the database for all of the zip codes associated with that state. It then passes that data back to the AJAX object.
Finally, the javascript receives the response to it’s AJAX request, and uses that data to populate the second dropdown.

Below is the actual code, along with some comments about what exactly, each piece is doing.

We start off inside your main controller. In my case, I put all of this in application/controllers/welcome.php.  This is the method that gets called to get the initial data, and to render the form. Basically it just gets the list of states, and renders the view that contains the form.

public function multi_drop() 
{
    $this->load->helper(array('url','form','html'));
    $this->load->model('zip_model', '', TRUE);
    $data['all_states'] = $this->zip_model->get_state_list();
    $this->load->view('multi_message', $data);
}

This method does the heavy lifting for getting the actual zip code data. This is the method that the AJAX request actually calls.

public function ajaxdrop()
{ 
    if($this->_is_ajax()){
        $this->load->helper(array('url','form','html'));
        $this->load->model('zip_model', '', TRUE);
        $state = $this->input->get('state', TRUE);
        $data['state_zips'] = $this->zip_model->get_state_zips($state);
        echo json_encode($data);
    }else{
        echo "Apparently is_ajax returned false!";
        show_error('This method can only be accessed internally.', 404);
    } 
} 

A couple of interesting points about the above code.

 if($this->_is_ajax())

refers to a function that checks to see if the method is being called by an AJAX request. This isn’t strictly necessary, it just keeps the method from firing unless it really is an AJAX request.

        $data['state_zips'] = $this->zip_model->get_state_zips($state);

is the call from the controller action to the model. This passes the value of $state to the model, so that the model can retrieve the zip codes for that state. This method them places that data into the “$data” array with the key “state_zips” which is how we’ll access it in the view. Note that this is a good place to add some data sanitization, since a bad guy could hack this so that the value of “$state” is “rm -rf /” or “DELETE * FROM USERS WHERE 1;”. Please remember that this code is not production ready.

This method is the final “action” that the form calls on submit. For now it doesn’t do anything except render another view.

public function handle_submission()
{
$this->load->helper(array(‘url’,’form‘,’html’));
$this->load->view(‘multi_response’);
}

Here’s the method that checks to see if it’s an AJAX request. Again, not necessary, but kind of cool.

/** 
* Checks to see if its an AJAX call.
*/ 
function _is_ajax(){
    return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'));
}

So, that’s it for the controller. Next, I’ll show you the model, although I’m not going to discuss it as thoroughly, since I would assume that your database is going to be much more interesting than a couple of tables with state abbreviations and zips codes.

function get_state_list(){                                                                                                                           
    $this->db->select('state_abbr');
    $query = $this->db->get('tbl_state');

    if ($query->num_rows() > 0)
    {   
      return $query->result();
    }else{
      return 'No States Found';
    }   
}                     

function get_state_zips($state){
    $this->db->select('zip');
    $this->db->where('state', $state);
    $query = $this->db->get('zips');
    log_message('info', "Value of state was: $state");
    if ($query->num_rows() > 0)
    {   
      return $query->result_array();
    }else{
      return 'No States Found';
    }   
} 

And that’s that. One method to get the initial list of states. One method that takes a state and returns an array of zip codes. Easy Peasy.

I’m not even going to bother with the views. Since they’re just a couple of CodeIgniter form-helper driven forms. Tell you what, though. I’ll package all of this up in an archive that you can download if you like. See the link at the end.

If you’ve lasted this long, then thank you for your patience. But I expect what you’re really after is the javascript. Well, here it is. Again, this is straight javascript, no jQuery involved. It should be pretty straight-forward to adapt this to your favorite framework.

(function() {                                                                                                                                        
    var httpRequest;
    dropper = document.getElementById("statedrop");
    dropper.onchange = function() { 
    makeRequest('http://codeigniter.technopoetic.com/index.php/welcome/ajaxdrop?state=' + dropper.value); 
    };

    function makeRequest(url) {
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            httpRequest = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // IE
            try {
                httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
            } 
            catch (e) {
                try {
                    httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
                }                 catch (e) {}
            }   
        }   

        if (!httpRequest) {
            alert('Giving up :( Cannot create an XMLHTTP instance');
            return false;
        }   
        httpRequest.onreadystatechange = alertContents;
        httpRequest.open('GET', url);
        httpRequest.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 
        httpRequest.send();
    }   

    function alertContents() {
        if (httpRequest.readyState === 4) {
            if (httpRequest.status === 200) {
                var data = JSON.parse(httpRequest.response);
                var select = document.getElementById('zipdrop');
                if(emptySelect(select)){
                    for (var i = 0; i < data.state_zips.length; i++){
                        var el = document.createElement("option");
                            el.textContent = data.state_zips[i].zip;
                            el.value = data.state_zips[i].zip;
                            select.appendChild(el);
                    }   
                }   
            } else {
                alert('There was a problem with the request.');
            }   
        }   
    }   

    function emptySelect(select_object){
        while(select_object.options.length > 0){                    
            select_object.remove(0);
        }   
        return 1;
    }   
})();

Again, I’m not going to really go into the esoteric points of this. For one thing, I’m not a javascript guru. For another, it’s up to you to figure out what you’re trying to do. But I will hit the high points.

First, of all this:

(function() {                                                                                                                                        
    ...
}()

wraps the entire thing in an anonymous function, for namespace/scope purposes. See this link, for a very good discussion of this technique: http://stackoverflow.com/questions/2421911/what-is-the-purpose-of-wrapping-whole-javascript-files-in-anonymous-functions-li

    var httpRequest;
    dropper = document.getElementById("statedrop");
    dropper.onchange = function() { 
    makeRequest('http://codeigniter.technopoetic.com/index.php/welcome/ajaxdrop?state=' + dropper.value); 
    };

In the above section of the code, we’re doing some housekeeping. We get the DOM element that represent the first dropdown, “statedrop”. Then we assign the “makeRequest” funtion to the onchange event for that element.

In the “makeRequest” function, we’re just setting up the httpRequest object. This is pretty standard code, and does some checking for browser compatibility. A quick Google for “AJAX Request” will turn up about 10M results with this exact code. The part to pay attention to is this:

        httpRequest.onreadystatechange = alertContents;
        httpRequest.open('GET', url);
        httpRequest.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 
        httpRequest.send();

This does a couple of things. First, it assigns the function “alertContents” to be run whenever the “Ready State” changes on the httpRequest object. Google “AJAX” if you’re not sure what that means. Another thing to note, is the “setRequestHeader” line. jQuery does this on it’s own, but this is what enables the backend to check whether this is an AJAX request or not. Finally, the meat of the code, is this:

    function alertContents() {
        if (httpRequest.readyState === 4) {
            if (httpRequest.status === 200) {
                var data = JSON.parse(httpRequest.response);
                var select = document.getElementById('zipdrop');
                if(emptySelect(select)){
                    for (var i = 0; i < data.state_zips.length; i++){
                        var el = document.createElement("option");
                            el.textContent = data.state_zips[i].zip;
                            el.value = data.state_zips[i].zip;
                            select.appendChild(el);
                    }   
                }   
            } else {
                alert('There was a problem with the request.');
            }   
        }   
    }   

This parses the result (which is returned by CodeIgniter as JSON), and propagates the second dropdown. The if statement, calls the “emptySelect” function, which just clears the existing option elements from the select dropdown. If you leave this out, then you’re appending new options, instead of replacing them. In some cases, that may be what you want, but for this, I wanted them replaced. Other than that, it’s pretty straightforward.

So, there you have it. One form dropdown whose values depend on another dropdown’s selected value. All driven by an AJAX request between the javascript on the page, and the CodeIgniter backend. This is just the basic technique, and it could be generlized to any combination of input elements, text boxes, checkboxes, radio buttons, etc… There are also multiple ways that you could structure the CodeIgniter methods as well as the javascript. This technique is intended to be a starting point for you to use in your own creations. Thanks for stopping by, and feel free to download the archive that contains the full controller, model, all the views, and the javascript. It can be downloaded as a zip file or as a tar.gzip file.

UPDATE:  A couple of people have asked for the database that I’m using here.  Frankly, I don’t remember where I got it, but it was the result of Googling something like “free US zip code list”.  In any case, you can download it here: zip_code_database