This is the documentation for concrete5 version 5.6 and earlier. View Current Documentation

A common requirement of many forms is gathering the user's country and state. Many times it is difficult to accommodate for all the different states and provinces that each country has, but luckily concrete5 has some built in helpers that, with the help of jQuery and AJAX, can be utilized to show the user the proper states for their selected country.

In this example I will be creating a single page called state_filter.php in the /single_pages directory and a controller for this single page located at /controllers/state_filter.php. Even though I am using a single page these instructions should be pretty much the same for a block, page type, etc.

The first thing to do is create the controller which will send the initial countries and states to the single page. Since the page is called state_filter the controller class needs to be StateFilterController. In this controller we need to load the states_provinces and countries helpers in the on_start() method. Using these helpers we can then set the states and countries for the view to use in the view() method. I also added another method called get_states() which will be the method that is called by AJAX and will be implemented later on. So my controller looks like this:

<?php
class StateFilterController extends Controller{

    public function on_start(){
        $this->stateHelper = Loader::helper('lists/states_provinces');
        $this->countryHelper = Loader::helper('lists/countries');
    }

    public function get_states(){
        //not yet implemented
    }

    public function view(){
        $this->set('countries', $this->countryHelper->getCountries());
        $this->set('states', $this->stateHelper->getStateProvinceArray('US'));
    }
}

You can see that by default I am passing the US states to the controller. You could send whatever states you like by default, but you will want to make sure that in your view you have the correct country selected by default, which I will demonstrate in a bit.

Now switching over to the state_filter.php single page we can implement the dropdowns for the countries and states using the concrete5 form helper. I am also going to add a textbox that will be displayed if the selected country does not have any states. My single page now looks like this:

<?php $fh = Loader::helper('form'); ?>

Country: 
<?php echo $fh->select('country', $countries, 'US'); ?>

<br/><br/>

State: 
<?php echo $fh->select('stateDropdown', $states, 'IL'); ?>

<input type="text" id="stateTextbox" style="display:none;" />

By default 'US' is selected (since we passed in US states) and I went ahead and set Illinois as the selected state. Now all that is left to do is write the javascript that will perform the AJAX call to retrieve the states when the country dropdown is changed, and the action in the controller that will return the states for the requested country.

First I'm going to implement the get_states() action in the controller. This action will require a country code (e.g. 'US') to be passed in that it will use to retrieve the states. Using that code and the states_provinces helper that is loaded in the on_start method we can get the states for the requested country. If the country does not have any states or provinces it will return null. So here is the code for the get_states() method:

public function get_states(){
    $country = $this->get('country');
    if(!empty($country)){
        $states = $this->stateHelper->getStateProvinceArray($country);
        echo json_encode($states);
    }
    exit;
}

And now, back in the state_filter.php single page, we need to write the javascript that will send the AJAX request to this action. So at the end of the page I added the following javascript (jQuery) code:

<script>
    $(document).ready(function(){
        $('#country').change(function(){
            $.ajax({
                url: '<?php echo $this->action('get_states'); ?>',
                type: 'GET',
                data: { country: $(this).val() },
                success: function(data){
                    var states = $.parseJSON(data);
                    var stateDropdown = $('#stateDropdown');
                    var stateTextbox = $('#stateTextbox');
                    if(states == null){
                        stateTextbox.show();
                        stateDropdown.hide();
                    }
                    else{
                        stateTextbox.hide();
                        stateDropdown.show()
                            .find('option')
                            .remove();

                        $.each(states, function(key, value){
                            var option = $('<option></option>')
                                            .text(value)
                                            .val(key);
                            stateDropdown.append(option);
                        });
                    }
                }
            });
        });

    });
</script>

In this script first I register an event handler for when the country dropdown list changes with this line: $('#country').change(function(){

This event handler only does one thing and that is send an ajax request using the jquery $.ajax() method to the get_states() action we just wrote. Notice that instead of hard-coding the link to the action I used:

url: '<?php echo $this->action('get_states'); ?>'

The data property then sets country (the variable to be passed to get_states()) to the current selected value of the country dropdown with this line:

data: { country: $(this).val() },

Finally, a callback function is added to the success property which will be called when the server successfully responds. Here I didn't do any error handling in the case that the server didn't respond with a success message, but in a real application you would want to handle errors properly. The data parameter that is passed to this function is the JSON that the get_states() method returns.

Inside the success function I then just check if the states object the server returned is null or not. If it is null then the states dropdown is hidden and the textbox is shown. This would allow a user to then type their own state/province if they wanted. If the states object is not null then the textbox is hidden and the state dropdown is shown. After showing the dropdown all of its options are removed so that it can be populated with the new states, which occurs in the $.each(states, function(key, value) loop.

So putting everything together, the complete controller should like like this:

<?php
class StateFilterController extends Controller{

    public function on_start(){
        $this->stateHelper = Loader::helper('lists/states_provinces');
        $this->countryHelper = Loader::helper('lists/countries');
    }

    public function get_states(){
        $country = $this->get('country');
        if(!empty($country)){
            $states = $this->stateHelper->getStateProvinceArray($country);
            echo json_encode($states);
        }
        exit;
    }

    public function view(){
        $this->set('countries', $this->countryHelper->getCountries());
        $this->set('states', $this->stateHelper->getStateProvinceArray('US'));
    }
}

And the complete single page should look like this:

<?php $fh = Loader::helper('form'); ?>

Country: 
<?php echo $fh->select('country', $countries, 'US'); ?>

<br/><br/>

State: 
<?php echo $fh->select('stateDropdown', $states, 'IL'); ?>

<input type="text" id="stateTextbox" style="display:none;" />

<script>
    $(document).ready(function(){
        $('#country').change(function(){
            $.ajax({
                url: '<?php echo $this->action('get_states'); ?>',
                type: 'GET',
                data: { country: $(this).val() },
                success: function(data){
                    var states = $.parseJSON(data);
                    var stateDropdown = $('#stateDropdown');
                    var stateTextbox = $('#stateTextbox');
                    if(states == null){
                        stateTextbox.show();
                        stateDropdown.hide();
                    }
                    else{
                        stateTextbox.hide();
                        stateDropdown.show()
                            .find('option')
                            .remove();

                        $.each(states, function(key, value){
                            var option = $('<option></option>')
                                            .text(value)
                                            .val(key);
                            stateDropdown.append(option);
                        });
                    }
                }
            });
        });

    });
</script>

This is a pretty simple implementation, and you may need to tweak it for your own needs, but hopefully this how-to has helped you understand how you can use concrete5's country and state helpers in your own forms.

Happy coding!

Loading Conversation