Archive: August, 2010

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.

Self-adjusting credit card expiration year

Once in a while, it happens, that you need to build a payment form with credit card expiration year as a select input.

With CakePHP’s form helper it’s pretty easy, but what’s even nicer is that you don’t have to hard-code any values for the actual years (and then have to remember to update them as the time goes on).

With the little snippet below we build a select input which starts and defaults to the current year and goes up to seven years ahead.

(So at the time of writing the select input range is: 2010 – 2017)

$this->Form->input('card_expiration_year', array(
                           'type' => 'date',
                           'maxYear' => date('Y', strtotime('+ 7 years')),
                           'minYear' => date('Y'),
                           'dateFormat' => 'Y',
                           'default' => date('Y')));

p.s. Why seven years? (Not sure, just like the number).

Make sure your app runs within the specified time zone…

Update: 8/2/2010 As red pointed out in the recent versions of cake this is handled through core.php:
http://github.com/cakephp/cakephp/blob/master/app/config/core.php#L247

For those stuck with the earlier versions of cake, but newer version of PHP (5.3+)… please see below:

Even with a simple shared hosting, the server time zone might differ from the time zone where you do business in, yet this becomes more important if you have multiple servers (perhaps even geo-distributed).

If you rely on fields like created and modified to do some basic reporting… or maybe you simply don’t wish to add/subtract server time difference against your region of business, the following one-liner will solve all your trouble.

In bootstrap.php:

date_default_timezone_set('America/New_York');

This is what I have in mine, since we are running on the Eastern Standard US time.
Of course, you’d set the zone per your specific requirement.