Dynamic select boxes with Rails 4

Introduction

I recently started doing some Rails development for a personal project (a wine cellar manager). I wanted to add a new wine to the database such that choosing the new wine’s country should update the list of available appellations to pick out for that country. The appellations are stored into the database and each appellation belongs to a country.

It turns out I did not find much info about that kind of feature; and most tutorials were either over-complicated stuff with a lot of JavaScript, or just outdated material targeting Rails 2 or 3.

Finally I thought that was a good opportunity to understand more deeply how a Rails application is constructed. At the time of writing, I used Rails 4 and Ruby 2.0. I tried to stick as much as possible to the Rails precept: convention over configuration. I don’t use any extra gems, and the code comes as Rails 4’s staple languages: CoffeeScript, Ruby and Embedded Ruby.

Ressources

The full app’s code has been released as a Rails project on GitHub; if at some point something fails in your local environment I would suggest you check first that your file contents match those on the reference. There is also a live demo running on Heroku of what we are about to build. Be sure to take a look at these links!

Code

The stub

Let’s start by generating the application stub and a basic controller.

rails new dynboxes

The models

We add the Country model; a country is a pair of a primary key and a name. Cities will reference their country through the primary key, or PK. Don’t worry about creating the PK, Rails handles this when inserting a new element into the database.

rails g model Country name:string

We also add the City model; a city is a triplet of a primary key, a name and a foreign key (FK) pointing to a country.

rails g model City name:string country:references

Add the following lines in app/models/city.rb, it details the association between Country and City. Depending on the order of the commands, these lines might have been automatically added during the generation.

# app/models/city.rb
 
class City < ActiveRecord::Base
  belongs_to :country
end

We can create the database’s schema with a couple of rake commands:

rake db:create
rake db:migrate

Let’s seed our database with a few country/city pairs — feel free to add some of your own pick :p Add yours in db/seeds.rb:

# db/seeds.rb
 
Country.create(name: "france")
Country.create(name: "italy")
 
City.create(name: "paris", country_id: Country.find_by(name: "france").id)
City.create(name: "nice", country_id: Country.find_by(name: "france").id)
City.create(name: "roma", country_id: Country.find_by(name: "italy").id)
City.create(name: "venezia", country_id: Country.find_by(name: "italy").id)

And run that rake command to populate the database with the new pairs:

rake db:seed

The controller

The controller is the corner stone of the application; we generate the main controller (this also generates an index view — this is our front-page):

rails g controller welcome index

We define 3 actions here — indexupdate_cities and show. The index responds to a front-page request, show responds to the form submitting, and update_cities responds to a select box update request when a new country is selected from the #countries_select select box. The app/controllers/welcome_controller.rb looks like:

# app/controllers/welcome/controller.rb

class WelcomeController < ApplicationController

  def index
    @countries = Country.all
    @cities = City.where("country_id = ?", Country.first.id)
  end
  
  def show
    @city = City.find_by("id = ?", params[:trip][:city_id])
  end
 
  def update_cities
    @cities = City.where("country_id = ?", params[:country_id])
    respond_to do |format|
      format.js
    end
  end

end

Don’t forget to declare the route for update_cities and show actions, add this line to your config/routes.rb:

# config/routes.rb
 
get 'welcome/update_cities', as: 'update_cities'
get 'welcome/show'

The views

Last thing to do is to create a couple of views to render our app. I propose to use dynamic select boxes in a form; this idea can be extended to build many other features however. Add the following lines into app/views/welcome/index.html.erb. Point out how we set HTML id to the select boxes here. The cities_select id is important: we will update _this_ HTML component with AJAX.

<!-- app/views/welcome/index.html.erb -->
 
<%= form_for :trip, url: {action: "show"}, html: {method: "get"} do |f| %>
  <%= f.select :country_id, options_for_select(@countries.collect { |country|
    [country.name.titleize, country.id] }, 1), {}, { id: 'countries_select' } %>
  <%= f.select :city_id, options_for_select(@cities.collect { |city|
    [city.name.titleize, city.id] }, 0), {}, { id: 'cities_select' } %>
  <%= f.submit "Go!" %>
<% end %>

The show view in app/views/welcome/show.html.erb prints out the city we just chose, this responds to the form action submit of the index view:

<!-- app/views/welcome/show.html.erb -->
 
<p><%= @city.name.titleize %></p>
<p><%= link_to 'Go Back', welcome_index_path %></p>

Last view to add is a partial rendering of the city in the #cities_select select box, which is called from the index view actually. We add an option entry with the city name and city primary key. We use a partial to keep things well separated and clean. In case your not acquainted with partials, a partial is a small piece of code to render one element in a collection; and Rails calls this code from a loop to render a full collection. The partial rendering in app/views/cities/_city.html.erb looks like:

<!-- app/views/cities/_city.html.erb -->
 
<option value="<%= city.id %>"><%= city.name.titleize %></option>

The AJAX magic

Now let’s write the core part, please have a cup of Coffee(Script)! Add the following lines in app/assets/javascripts/welcome.js.coffee. Basically this code sends an AJAX GET request to that URL /update_cities with country_id in parameter. Then we use this parameter to return a list of cities belonging to the relevant country.

# app/assets/javascripts/welcome.js.coffee
 
$ ->
  $(document).on 'change', '#countries_select', (evt) ->
    $.ajax 'update_cities',
      type: 'GET'
      dataType: 'script'
      data: {
        country_id: $("#countries_select option:selected").val()
      }
      error: (jqXHR, textStatus, errorThrown) ->
        console.log("AJAX Error: #{textStatus}")
      success: (data, textStatus, jqXHR) ->
        console.log("Dynamic country select OK!")

Exactly, the update_cities action responds as a JavaScript, which is in app/views/welcome/update_cities.js.coffee:

# app/views/welcome/update_cities.js.coffee
 
$("#cities_select").empty()
  .append("<%= escape_javascript(render(:partial => @cities)) %>")

This CoffeScript is straightforward: empty the cities_select HTML component, then append to the response some cities from the City collection. It’s important to point out that the select box is “HTML-repopulated” by calling the partial we have declared previously in the views.

Last words

Please feel free to fork the reference on GitHub and do push-requests with your improvements. Alternatively you can post your feedback in the comment section, thanks!

Advertisements

52 thoughts on “Dynamic select boxes with Rails 4

  1. Hi, thanks for code.
    1. Looks like its not working with Crome.
    2. After changing coffee to
    $ ->
    $(document).on ‘change’, ‘#countries_select’, (evt) ->

    works.

    1. Hi there! I tried with Chrome Version 34.0.1847.116 m and I didn’t see any issues here. I will check tonight how ‘change’ and ‘click’ differ in behaviour though.

      Thanks for the feedback!

  2. On change is a better way to go, the event is fired after it has been changed. Pressing “S” to select Spain will select the country without firing your JS.

    1. Hi Ryan,

      I modified the code accordingly to use ‘change’ instead of ‘click’ as you proposed. That’s indeed much better; thank you!

  3. This was just what I needed! I needed to implement something similar, but with dynamic radio buttons for categories and sub-categories, for creating a product listing. Just some minor tweaking and I got it working pretty quick.

  4. Benoît, this is extremely helpful. Thanks for a concise tutorial!

    I’m using this project to start learning AJAX and Coffeescript, and I appreciate your explanations of what each code block does.

    One thing I’d like to see is a way to chain more than two selects together, and the ability to hide the following dependent select until the user has made the initial selection.

  5. Benoît, just wanted to chime back in. I used your tutorial to build a dynamic group of 6 selects. Each one queries a model using the call Model.where(:criteria => select_input1).pluck(:next_select_options).uniq. For deeper selects, I just chained more .where calls with additional criteria.

    The page is working perfectly, and I wouldn’t have been able to do it without your post.

    Thanks!
    Brennan

  6. I had some obstacles to overcome in implementing this as I am doing my first real bonafide RoR project. After sticking with it, I finally managed to adapt this code to my purposes. Persistence prevails. Getting this working was a good learning experience.

  7. I cannot understand where is this partial called app/views/cities/_city.html.erb

    and I cannot understand why you are passing a @cities collection to app/views/welcome/update_cities.js.coffee

    how is the partial linked with that piece of coffee script?

  8. Hi,

    I have implemented dynamic drop down, it worked for me in “new” page and failed when calling form and “index” page

    I have a parent drop down called “projects” and child dropdown called “tests”. Upon selecting a project, the tests drop down should be updated.

    In “new” page (view), when I select the parent drop down is calling “/results/update_tests?xxxxx”. However when I selected the project in “index” form, “/update_tests?” is called and failed with the following error:

    ActionController::RoutingError (No route matches [GET] “/update_tests”):

    Here is My routes configuration:
    get ‘results/update_tests’, as: ‘update_tests’
    get ‘results/update_manifests’, as: ‘update_manifests’

  9. Great write up for Rails 4 and it works great with stored data. What I”m struggling with currently is applying what you’ve done here, but with JSON data objects in the select menus. How would I pass or compare the AJAX returned params(i.e. where you have ‘@cities = City.where(“country_id = ?”, params[:country_id])’) ? Thanks for your good work!

  10. Hi!, great tutorial thank you very much!.

    I just have a question that I am trying to add the 3 drop down list related with the cities, lets call it districts. But I could not combine 3 of them in ajax call. Thank you

  11. Great tutorial. It worked perfectly. I’d like to echo some of the previous comments suggestions of creating a tutorial where you add a 3rd dependent drop down list.

    Anyways, thanks for this. It was really helpful!

  12. Got this working! One thing though: I’m trying to style the dropdowns, and adding a class breaks the Ajax request. How do you apply a custom style to the drop down element without breaking the ajax call?

  13. Is it possible to do this without AJAX? I’ve figured out how to implement custom select elements, but the one I want requires some javascript that alters the HTML. When the HTML gets updated, it breaks the AJAX request.

  14. Great got this working though I noticed that it only works for select where the key field is called id and the text field is called name. I had a to change the file dynamic_selectable.js.coffee which means the routine is not generic. I guess there must be some way I can set some attributes that allow me to specify the key field and name field

  15. Thanks for probably the best post for this subject. Your example works fine. In my project I need similar construction. But unfortunately I can’t move this code to my project (because not enough skills). The main problem that I don’t understand the construction (render(:partial => @cities)) from app/views/welcome/update_cities.js.coffee (what is @cities? instance variable from controller? or folder under /app/view/welcome?). Sorry for foolish question.

    1. HI Sergyey,

      @cities come from these lines:

      def index
      @cities = City.where("country_id = ?", Country.first.id)
      end

      And you can easily add cities with db/seed.rb:

      City.create(name: "roma", country_id: Country.find_by(name: "italy").id)

      1. Hi, Benoit
        Thanks a lot! Then could you please tell me where in your code called cities/_city.html.erb? I have line: Rendered cities/_city.html.erb in your log file and don’t have similar line in mine.

  16. Hi, Benoit
    I have found error in my code (put .haml instead .coffee in update_cities). Thanks for your help. Do you have any idea how to keep selected values after reload page (for example after failed validation of other field)?

  17. Hey Sergey,

    I seem to be running into this error in the show action for my welcome controller:

    undefined method `[]’ for nil:NilClass

    I’m assuming it has something to do with the “:trip” params?

  18. Thanks for this. I have it working. Is it the ‘render partial => @cites’ ‘ that forces the cities/_city.html.erb to be repetetively applied for each city? I’ve not seen this syntax before.

  19. Hi, I’m following every step in this tutorial, but for some reason the cities select tag is not changing based on the country select tag.

  20. I want to populate the third dropdown list from second dropdown list while I am getting populated second dropdown list but not getting the third one plz help me out ?

  21. I have an error message when I change countries select menu:

    Started GET “/update_cities?country_id=2&=1502903322680″ for 10.34.82.2 at 2017-08-16 10:08:45 -0700
    Processing by UploadfilesController#update_cities as JS
    Parameters: {“country_id”=>”2”, “
    “=>”1502903322680”}
    City Load (0.3ms) SELECT cities.* FROM cities WHERE (country_id = ‘2’)
    Rendered uploadfiles/update_cities.js.coffee (2.3ms)
    Completed 500 Internal Server Error in 5ms

    ActionView::Template::Error (Missing partial cities/city with {:locale=>[:en], :formats=>[:js, :html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
    * “/nfs/site/disks/ch_icf_techpubs_001/users/skochhar/ror/storefront/app/views”
    ):
    1: $(“#cities_select”).empty()
    2: .append(” @cities)) %>”)
    app/views/uploadfiles/update_cities.js.coffee:2:in _app_views_uploadfiles_update_cities_js_coffee__1238243312005805556_45978580'
    app/controllers/uploadfiles_controller.rb:32:in
    update_cities’

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s