views:

625

answers:

3

I have developed contingent country-state select dropdowns, and I'd like to factor out this behavior to an Address model so that I don't have to copy the dynamic behavior (more or less split between view and controller) each and every time I want the user to be able to enter a full address.

Basically I'm trying to dip my toes a little deeper into DRY. But I'm not sure exactly where to embed the behaviors. Do I use the model or the helper to build the necessary forms? Most importantly: where and how can I invoke the dynamic behavior for updating lists of states? Do I need an Address controller, or could it all be done from within the model?

In other words, what I've got now in the view is something like:

  # _refine.html.erb
      <tr>
        <td>
        <%= label_tag :dest_country, 'Country: ' %></td><td>
          <%= select_tag :dest_country, 
            options_for_select(Carmen::country_names 
                      << 'Select a country', 
                :selected => 'Select a country'), 
            {:include_blank => true,
            :id => 'country_select',                              
            :style => 'width: 180px',
            :onchange => remote_function(
              :url => {:action => 'update_state_select'},
              :with => "'country='+value")} %>
        </td>      
      </tr>
      <tr>
          <div id="state_select_div">
            <td><%= label_tag :dest_state, 'State: &nbsp&nbsp' %></td>
            <td><%= select_tag :dest_state, 
                        options_for_select(Carmen::states('US').collect{
                                |s| [s[0],s[0]]} << ['Select a state'], 
                                :selected => 'Select a state'), 
                                {:style => 'width: 180px'} %></td>
          </div>      
      </tr>

The update method is in the controller:

# search_controller.rb

def update_state_select
  # puts "Attempting to update states"
  states = []
  q = Carmen::states(Carmen::country_code(params[:country]))
  states = q unless q.nil? 
  render :update do |page|
    page.replace_html("state_select_div",
    :partial => "state_select",
    :locals => {:states => states }
  )
 end
end

Finally, I've got a partial which is dropped in with the proper names or a blank text field:

# _state_select.html.erb
<% unless states.empty? or states.nil? %>
      <%= label_tag :dest_state, 'Select a state'  %>
 <br/> <%= select_tag :dest_state, 
                     options_for_select(states.collect{|s| [s[0],s[0]]}  
                         << ['Select a state'], 
                       :selected => 'Select a state'), 
                       {:style => 'width: 180px'} %>
 <% else %>
   <%= label_tag :dest_state, 'Please enter state/province' %><br />
   <%= text_field_tag :dest_state %>
<% end %>

Now, what I'd like to do is to be able to associate an address through the model (say, Person has_one :address) and within the form for creating a new person, be able to use something like

 <%= label_tag :name, 'What's your name?' %>
 <%= text_field_tag :name %>
 <%= label_tag :address, 'Where do you live?' %>
 <%= address_fields_tag :address %>

Which could generate the appropriate drop-downs, dynamically coupled together, the results of which would be accessible through Person.address.country and Person.address.state.

Thanks in advance!

+1  A: 

Data belongs to models and presentation belongs to templates (and helpers). That said I would start separating data from presentation and removing useless code.

states.collect{|s| [s[0],s[0]]}

AFAIK, this execution is not required. You can simplify it to

states.collect{|s| s[0]}

or

states.collect(&:first)

but most important, you can provide a custom method in your Carmen class to get the data in the correct format.

Also the following code could be simplified using the :prompt => "Select a State" option so that you don't need neither to append a new array item, nor to manuually select the item called "Select a State".

options_for_select(Carmen::states('US').collect{
                                |s| [s[0],s[0]]} << ['Select a state'], 
                                :selected => 'Select a state'),

Finally, you might want to encapsulate complex helpers in object to make the easier to be tested with a Test::Unit suite. Check out this screencast.

Talking about the Controller, change

# puts "Attempting to update states"
states = []
q = Carmen::states(Carmen::country_code(params[:country]))
states = q unless q.nil?

into

# puts "Attempting to update states"
states = Carmen::states(Carmen::country_code(params[:country])) || []

Be sure to not call two methods when you can call just one. Change

<% unless states.empty? or states.nil? %>

into

<% unless states.blank? %>
Simone Carletti
Thanks! These are all great suggestions which I'll implement, but I'm trying to discover how I'd refactor the dynamic update behavior out of the view/controller and into an Address model, helper or module.
Joe
http://railscasts.com/episodes/88-dynamic-select-menus
Simone Carletti
Right, I've got the functionality exhibited in that cast working properly. My question is how to replicate this functionality generically, i.e., in a DRY way. To wit: * Anytime I need to gather an address, I should be able to associate an Address with the model * When I'm gathering/updating that address, I should be able to use helper methods to generate the address part of the form (which includes invoking the dynamic update of certain fields.)I don't want to copy-paste the dynamic behavior whenever I need it; I've already fine-tuned it and want to refactor it out.
Joe
+1  A: 

Let me first make sure I understand the code correctly; when a user changes the Country select box, and AJAX request is made to the server, which then sends back a script that updates the States box with the correct entries?

If so, I'd use a completely different strategy. Use a callback that's triggered when dest_country is changed. Let this callback do an AJAX GET request to e.g. /countries/{COUNTRY}/states.json (or .xml). You need to have a route and controller set up for this, of course. Populate dest_state with the returned values.

UPDATE: As for factoring out user-interface code, try using partials and helpers. http://guides.rubyonrails.org/layouts_and_rendering.html#using-partials

Daniel Schierbeck
Thanks. Any resources on callbacks?
Joe
Note that I really need a little more than just "try helpers and partials," I want to know HOW to factor out the contingent dynamic behavior into a plugin, module or model.
Joe
A: 

I have written a blog post on this using Carmen and jQuery. The code is reusable because it lives in a partial in my addresses views. I use it by a fields_for "address" tag in the form. You can read the entire entry here: http://eric.lubow.org/2009/ruby/rails/country-state-select-using-carmen-and-jquery/.

Eric Lubow