zen of coding

Solving the country/state problem with CakePHP and jQuery

I’d say that it is a pretty common issue (and maybe not quite “a problem”), where you would have a page with a billing form, which in turn, has a “Country” select as well as a “State” select.
Obviously not all countries have states, and thus comes the question of how to handle the situation.

Before going much further, I’d like to say that the whole point of the post is to show a solution to the common problem, but by no means it is the de facto standard of handling the form fields (as a matter of fact, I’ll say that there simply isn’t one).
If you consider every billing form that you’ve had to fill out in your internet life, each one, most likely, behaved differently in some way from the others.
Country, for which the application is purposed, and thus the target audience, not to mention the specific business needs of the application should guide the actual implementation.

For this example, we’ll do something like this:

  1. If selected country is “US” show the select input with the states
  2. For any other country change the select to a text input (allowing the user to enter what they please, if required)

Hmmm… a seemingly simple solution quite often requires a little bit of hackery. Or does it?
For security (or browser-related) reasons you cannot change an input type on the fly, otherwise it would be a perfectly reasonable thing to do.

… So how, after all, do we tackle this with cake and jQuery, without hacking around the problem?

First let’s consider this little snippet of a CakePHP form (view):

<div id="state-wrapper">
  <?php echo $this->Form->input('state', array('id' => 'state-select')); ?>
  <?php echo $this->Form->input('state', array('type' => 'text', 'id' => 'state-text')); ?>
</div>

As you see we have two nearly identical inputs. (Please note the different DOM id’s for later).
Also, you can guess which one is the select and which one will be a text input ;)

Now comes the fun jQuery code:

$(document).ready(function()  {
  stateText = $('#state-text').detach();
  stateSelect = $('#state-select').detach();

  checkSelected(stateText, stateSelect);

  $('#country-select').change(function() {
    checkSelected(stateText, stateSelect);
  });
});

function checkSelected(stateText, stateSelect) {
  if ($('#country-select').val() != 'US') {
    stateText.appendTo('#state-wrapper');
    stateSelect.detach();
  } else {
    stateSelect.appendTo('#state-wrapper');
    stateText.detach();
  }
}

Let’s take a closer look at this…
Upon the initial load of the page we detach() both the select and text inputs from the DOM.

First of all… what is detach()?
Yet, another beauty of jQuery, which was introduced sometime in 1.4.
The detach() method actually removes the element from the DOM… but (unlike a more common remove()) it keeps all the element’s properties, data, associated jQuery events and everything else that comes with it. This allows us to later inject the “detached” element, as though it never “left”, back into the DOM.

So, once both elements are “detached”, we go ahead and check the currently selected country.
Here, the code should be pretty simple:

  1. If country does not equal “US”, show (via appendTo()) the text input
  2. Otherwise, show the select input (with all the pre-loaded states, as they were before the “detachment”).

Ah, The power of jQuery.

p.s. A more elegant solution would be to keep a list of states/provinces/regions/etc. for alll countries, which require one.
With that we could populate the state select via AJAX once such country is selected… or remove the field altogether. Surely this option would provide more consistency and data integrity, but introduce a bit of maintenance overhead. However, I’ve already showed this approach in an older post, so we’ll just leave things a little different and simpler this time.

  • Javier

    “A more elegant solution would be to keep a list of states/provinces/regions/etc. for alll countries, which require one. (…)”

    I’ve used this solution in some projects, and as you say, it’s more complex. I only find it useful if there really are users from all over the world. If most of the users are from one country or two, I generally prefer just leaving the state field optional (as a text input, as you say in this article) for other countries.

    By the way:

    if ($(‘#country-select’).val() != ‘US’) {
    (…)
    else if ($(‘#country-select’).val() == ‘US’) {

    Looks like this condition is redundant.

  • teknoid

    @Javier

    Regarding the condition… one is “not equal” the other one is “equals”… unless I am missing something.

    • Gordon

      if (country = ‘US’) then
      blah
      else // Country obviously != US, so why bother with an if :p
      blah

  • red

    Very nice article! Thanks teknoid!
    Regarding condition – I think (and probably Javier too) will be easier like that:

    if ($(‘#country-select’).val() == ‘US’) {
    // here goes select
    } else {
    // here goes text input
    }

  • teknoid

    @red, Gordon…

    Sure, a simple “else” would do… but I had a few additional conditions where I copy/pasted the code so that’s where that extra else if() came from.
    Unnecessary, I agree… but not much harm done.

    And thank you for pointing this out.

  • Arvind K.

    Nice article. Thank you, for displaying the power of detach() in particular.

%d bloggers like this: