Best way to client-side load or hide state/province drop down when country is changed?

Maybe I’m just trying too hard, because I have done this plenty of
times with plain old javascript before Rails entered my life. But now
that I’m trying to work within the Rails system of form_for and such,
I am finding that what should be a simple task has become arduous
beyond compare. I’m ready to tear out what little hair I have left,
and I have yet to find anything online that addresses what I thought
would be a common scenario.

So, the scenario:
A user create/edit page. When a user selects US or Canada, the drop
down loads accordingly. If he selects a different country, the
dropdown disappears. When the form is submitted, the user and his
address are saved accordingly.

User: has_one :address
Address: has_one :state, has_one :country

edit.rhtml:

<% form_for :user, :url => {:action => “update”, :id => @user} do |f|
%>
<%= render :partial => “user_form”, :locals => {:f => f} %>
<% end %>

_user_form.rhtml:

First name: <%= f.text_field :first_name %>

Last name: <%= f.text_field :first_name %>

<% fields_for :address do |a| %>

City: <%= a.text_field :city %>

Country: <%= a.select :country_id, COUNTRIES %>

State: <%= a.select :state_id, STATES %>

<% end %>

At first, I tried putting the state selector and text in its own
partial, thinking that via a custom onchange event on the country
selector and by using RJS I would be able to reload the state selector
from a controller method or hide it altogether. But in practice it
can’t be done because the state selector needs a form reference and
the controller can’t pass that in. It got ugly real fast.

So where I stand now, I’m looking at writing a custom javascript
method, fired from an onchange event on the country selector, that
would reload the contents of the state selector. Have I missed some
magical rails ingredient or helper that would make this better? Has it
just been too long a day? Any help appreciated.

I got it working by foregoing a form_for selector for this attribute,
and using a manually loaded html selector instead. Seems to work as
desired, though it feels a bit hackish.

i dont think that’s ‘hackish’.
Basically you’re foregoing some automated magic for more control and
specificity
which in this case you needed.

True true! Just happy to get it working.

Yuval wrote:

True true! Just happy to get it working.

yuval raz?

If you want a solution that is more Rails-centric…

First, I created some helper methods just for convenience. (I only
needed the state_options one but created the others for testing this
solution). The xxx_options simply creates an acceptable collection
for populating a Rails select. The xxx_option_tags return a string of
option tags with the line breaks removed. That’s necessary for their
inclusion in a js function below:

in application_helper.rb:

def state_options
Address::STATES.collect{|state| [state, state]}
end

def province_options
Address::PROVINCES.collect{|state| [state, state]}
end

def state_option_tags
options_for_select(state_options).gsub("\n", “”)
end

def province_option_tags
options_for_select(province_options).gsub("\n", “”)
end

I’ve got an address partial because I will capture addresses for all
kinds of different things (people, customers, businesses, etc). My
address partial just consists of a normal ‘fields_for :address’.
Here’s the part relevant to the discussion:

<%= f.select :state, state_options %>
<%= f.text_field :postal_code %>
<%= f.country_select :country, ['United States',

‘Canada’], :selected=>‘United States’ %>
<%= observe_field :address_country,
:function=> “switch(value){ case ‘United
States’: $(‘address_state’).show();$
(‘address_state’).update(’#{state_option_tags}’); break; case
‘Canada’: $(‘address_state’).show();$
(‘address_state’).update(’#{province_option_tags}’); break; default: $
(‘address_state’).hide();}” %>

Okay, so what’s going on… I’m making use of the prototype library
packaged with Rails and the baked-in “observe_field” helper. This
helper observes the country select field and fires on a change (the
country select is rendered with US and Canada at the top before an
alphabetical listing of countries, and US is selected by default).
The :function=>… part is passing a js-function that will become the
method responsible for handling the change in the observe field. It
will be wrapped in a function block that looks like: function(object,
value) {}. That’s where the ‘value’ field comes from. The rest of
the string is just a switch-statement that loads the appropriate
values into the select box or hides it all together.

The #{state_option_tag} is where the application_helper.rb comes into
play. That is evaluated at run time to give the select options you
need. The net effect is that the js swaps out the tags in
the address_select field.