Thursday, December 3, 2009

Rails Country and States Fields Made Easy

I've been writing Ruby code for just over a year now. It never ceases to amaze me how a fresh perspective can really put you on the right track sometimes. The Ruby programming language has been that kind of fresh perspective for me. Still, at times you have to tackle the same old problem once again. That's what happened to me when it came time to create the dreaded country and state combo box code once again. It seems I've written this code at least a dozen times in the last 24 years of creating applications. Sometimes it's been for client desktop applications and sometimes for web based but it is always the same and always boring. Not so this time! Even if I had to write it myself, which I started to do, it was going to be new because I'm still learning Rails and at this point loving it. Yes, I know, can you say MVC framework? I can but this one's in Ruby! What a difference. I'm off my soap box now.

If you need to a country and state solution for Rails and you've used Google to find some information and examples you know this topic has been beat to death. I almost gave up and started to have some fun writing it all myself. (You know, that dreaded pull the ISO table from somewhere online and create the SQL inserts followed by JavaScript and some…. (You get the picture). Then I stumbled across a Railscasts episode #88 and a GitHub project called Carmen and voila! My solution came together quickly. New and fresh yet still the same old thing.

Here's how you get this going.

First watch the Railscast episode #88. Way to go Ryan Bates! (RailsCasts has been a life saver this year.)

I decided to implement the dynamic JavaScript exactly like Ryan shows. However, I did have to change his JavaScript slightly for a number of reasons. These are my changes:

#dynamic_states.js.erb

var states = new Array();
<% for state in @states -%>
  states.push(new Array("<%= state[0] -%>", "<%=h state[1] -%>", "<%= state[2] -%>"));
<% end -%>

function countrySelected() {
  country_id = $('<%= @country_dom_id %>').getValue();
  options = $('<%= @state_dom_id %>').options;
  
  indx = $('<%= @state_dom_id %>').selectedIndex;
  if (indx > 0 && indx < options.length) {
      curr_value = options[indx].value
  } else {
      curr_value = ''
  }
  options.length = 0;
  options[options.length] = new Option("Select a State","")
  states.each(function(state) {
    if (state[0] == country_id) {
      opt = new Option(state[1], state[2]);
      if (state[2] == curr_value) {
        opt.selected = true
      }
      options[options.length] = opt
    }
  });
  if (options.length == 1) {
    $('<%= @state_parent_dom_id %>').hide();
  } else {
    $('<%= @state_parent_dom_id %>').show();
  }
}

document.observe('dom:loaded', function() {
  countrySelected();
  $('<%= @country_dom_id %>').observe('change', countrySelected);
});

I needed to change the code that pushes the values on the states[] array. My values are coming from an array and they'll be string data as you'll see from the Carmen project later on.

I also needed to fix it so that if I supplied a selected value for the State list it wouldn't get stepped on by the JavaScript immediately calling the countrySelected()method. This was something Ryan added after the episode was produced (see his show notes) which sets up the fields properly the first time.

And last, I needed to supply DOM ids for my fields and because I have two models that have state and country values. I know this is not very DRY but it is what I have for now and I'm certain to re-factor it soon.

Next, take a trip over to the Carmen project on GitHub (thanks Jim, next time I'm in Chicago I owe you a beer) for a simple file system based approach to the Country and State data you'll need. It's actually nice not having to maintain the DB tables for this again. I might re-think that later but for the project I'm on right now this is just perfect. I stuck this in my vendor/plugins folder for the project I'm working on and then changed my view and the Javascripts_Controller class. Here's the code:

#Javascripts_Controller.rb

class JavascriptsController < ApplicationController
    def dynamic_states_for_user
        @states = getStatesArray
        @country_dom_id = 'user_country'
        @state_dom_id = 'user_state'
        @state_parent_dom_id = 'state_field'
        render :action => 'dynamic_states'
    end

    def dynamic_states_for_contact
        @states = getStatesArray
        @country_dom_id = 'contact_country'
        @state_dom_id = 'contact_state'
        @state_parent_dom_id = 'state_field'
        render :action => 'dynamic_states'
    end

    private 
    def getStatesArray
        @states = Array.new
        Carmen::STATES.each do |country|
            id = country[0]
            list = country[1]
            list.each do |state|
                @states.push([id, state[0], state[1]])
            end
        end
        return @states
    end
end
I simply built an array for the @states variable from the Carmen::STATES data. I could have probably adjusted the view to loop through this data directly or maybe even changed the JavaScript but as you can see, I didn't.

Now for the final steps, all you need to do is adjust your views to contain the handy dandy Carmen view helper methods and you're done.

#new.html.erb

<% javascript 'dynamic_states_for_user' %>
<% form_for @user, :url=> { :action => "create"} do |f| %>
<%= f.error_messages %>
<%= render :partial => "form", :object => f %>
<div><br/><%= check_box_tag("send_reg_email","true") %><%= label_tag('send_reg_email','Send welcome registration email to user?')%></div>
<div><br/><%= f.submit "Save" %> <%= submit_tag 'Cancel', {:confirm => 'Cancel changes?', :name => 'cancel'} %></div>
<% end %>

#_form.erb (my users record partial)

<tr>
    <td><%= human_label User, :country %>: </td>
    <td><%= country_select(:user, :country, nil, {:prompt => "Select a Country"})%></td>
</tr>
<tr id="state_field">
    <td><%= human_label User, :state %>: </td>
    <% if @user.country && Carmen::states?(@user.country) -%>
    <td><%= state_select(:user, :state, @user.country, {:prompt => "Select a State"}, { :style => "width: 230px;"}) -%></td>
    <% else -%>
    <td><%= select( :user, :state, options_for_select([],""), {:prompt => "Select a State"}, { :style => "width: 230px;"})-%></td>
    <% end -%>
</tr>
<tr>
    <td><%= human_label User, :zipcode %>: </td>
    <td><%= form.text_field :zipcode, :size => 15 %></td>
</tr>

That's all it takes to get the dynamic client side JavaScript implementation of Country and State paired select fields. Enjoy!

References

Railscasts – Episode #88

Jim Benton – Carmen on GitHub

Jim Benton – His blog with some other cool stuff.

SQL If you decided to go the DB route instead

No comments:

Post a Comment