Absolute Moron's Guide to Forms in Rails, Part 5 22

Posted by jeff Thursday, April 10, 2008 13:05:00 GMT

List Boxes in Rails

Today we will tackle the more thornier controls: list boxes and combo boxes. Let’s add the ability to select the origin and destination airports for our flight from a list of available airports.

Airports

We need a small list of airports to choose from, so let’s create a lookup table.

script/generate scaffold Airport code:string
rake db:migrate

Use the generated scaffold UI at /airports to add a few airports. Here’s what I chose:

We also need add two columns to our flight model:

script/generate migration AddAirportsToFlight origin_id:integer destination_id:integer
rake db:migrate

Notice that we’re storing integers, not strings this time. This is so we can hold foreign keys to the appropriate rows in the Airports table. We also need to open up the flight.rb model file and specify the relationship between flights and airports.

class Flight < ActiveRecord::Base

  belongs_to :origin, :class_name => "Airport" 
  belongs_to :destination, :class_name => "Airport" 

end

If you’re not very familiar with ActiveRecord yet, the code here uses a slightly more advanced syntax of the belongs_to method since the lookup table class name is not related to the attribute names we want in our Flight class. (If Rails knew that “origin_id” and “destination_id” are intended to be foreign keys to the Airports table, that would be magic indeed).

Similarly, let’s hook things up on the Airport side of the equation:

class Airport < ActiveRecord::Base

  has_many :departures, :class_name => "Flight", :foreign_key => "origin_id" 
  has_many :arrivals, :class_name => "Flight", :foreign_key => "destination_id" 

end

Before we use any GUI sugar, open up your console and hook up a flight to two airports (I’ve omitted a bunch of the console output for clarity):

>> f = Flight.find(1)
=> #<Flight id: 1, number: "123", created_at: "2008-04-05 20:05:24", updated_at: "2008-04-05 20:05:24", meal: false, equipment: nil, origin_id: nil, destination_id: nil>

>> ord = Airport.find_by_code 'ORD'
>> pdx = Airport.find_by_code 'PDX'

>> f.origin = ord
>> f.destination = pdx
>> f.save!
=> true

And now, behold the beauty and power of ActiveRecord associations that give us a natural-language vocabulary for our models.


>> ord.departures
=> [#<Flight id: 1, number: "123", created_at: "2008-04-05 20:05:24", updated_at: "2008-04-06 10:22:23", meal: false, equipment: nil, origin_id: 1, destination_id: 2>]
>> pdx.arrivals
=> [#<Flight id: 1, number: "123", created_at: "2008-04-05 20:05:24", updated_at: "2008-04-06 10:22:23", meal: false, equipment: nil, origin_id: 1, destination_id: 2>]

Awesome!

Now let’s see how we can make those associations in our form instead of through the console.

Selecting Airports

In HTML, listboxes and combobox (drop-down list) controls are both known as selection lists, and are implemented by the <select> tag. Nested inside the <select> tag are one or more option tags. Each option describes one list item.

The form_for builder gives us two waysto construct selection lists without having to manually construct each <option> tag individually. One is the select helper, which provides full control over how the <select> and <option> tags are generated. Another is collection_select, which provides a simpler syntax if:

  • You can provide the list of items as a Ruby collection
  • Each item in the collection has a method that can be called to provide a displayable string for that item
  • Each item also has a method that can be called to supply the value that should be sent to the application when the item is selected by the user.

These three conditions are the 80% case when you’re constructing the list from an ActiveRecord table. Some code will make it clearer. Open up new.html.erb again and add this code into the form:


<% airports = Airport.find(:all, :order => :code) %>
<p>Depart from: 
  <%= f.collection_select :origin_id, airports, :id, :code %>
</p>

<p>Arrive at: 
  <%= f.collection_select :destination_id, airports, :id, :code %>
</p>

We start by capturing all of the airports into the airports variable. Note that we use the <% %> syntax, without the equal sign, whenever we don’t want the result to be sent back to the browser.

We then use the collection_select helper to construct our combo box. Here are the parameters we had to pass:

  • First is the attribute of our model we’re trying to assign to (origin_id or destination_id)
  • Second is the Ruby collection. We will get one <option> tag – one item in the list – for each object in the collection.
  • Third is the method on the collection items that should be called on the selected item when it’s time to transmit the selection to the application.
  • Last is the method on the collection items that should be called to generate that item’s displayed string in the list.

Navigate to the form in your browser:

Finally, let’s make sure we understand how this ends up in our new flight model. Select some values and click Create.

Back To That Bridge Again

In my case, I created flight #444 from Chicago to Portland on a Boeing 777. Take a look at your log file (log/development.log) and find where the create action was called. Mine looks like this:

Processing FlightsController#create (for 127.0.0.1 at 2008-04-06 14:31:39) [POST]
  Session ID: BAh7BzoMY3NyZl9pZCIlZmRhM2FmODI0ZjA1MjYwMzgxMGJiMjcyYjBiOTNi%0AMTUiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%0Ac2h7AAY6CkB1c2VkewA%3D--273a533fb4cee21ea44cb1ccdc6d70db908d7525
  Parameters: {"commit"=>"Create", "authenticity_token"=>"e9e1c0b0d744f33db348cd70a69cf70d6126c66b", "action"=>"create", "controller"=>"flights", "flight"=>{"number"=>"444", "origin_id"=>"1", "meal"=>"0", "destination_id"=>"2", "equipment"=>"747"}}
  Flight Create (0.000350)   INSERT INTO flights ("updated_at", "number", "origin_id", "meal", "destination_id", "equipment", "created_at") VALUES('2008-04-06 14:31:39', '444', 1, 'f', 2, '747', '2008-04-06 14:31:39')
Redirected to http://localhost:3000/flights/4
Completed in 0.00958 (104 reqs/sec) | DB: 0.00035 (3%) | 302 Found [http://localhost/flights]

Zooming in on just the parameters and reformatting for clarity:

Parameters: { "commit"=>"Create",
                   "authenticity_token"=>"e9e1c0b0d744f33db348cd70a69cf70d6126c66b",
                   "action"=>"create", 
                   "controller"=>"flights", 
                   "flight"=> { "number"=>"444", 
                                "origin_id"=>"1", 
                                "meal"=>"0", 
                                "destination_id"=>"2", 
                                "equipment"=>"747"}
                              }

See how all of our form data is in the tidy flight hash? We can easily access all the form values with params[:flight]. The important thing to notice are the values that came across for the selected airports.

Here’s another way to confirm to ourselves that it worked. Open up the Rails console again and find the flight:

>> f = Flight.find_by_number '444'
=> #<Flight id: 4, number: "444", created_at: "2008-04-06 14:31:39", updated_at: "2008-04-06 14:31:39", meal: false, equipment: "747", origin_id: 1, destination_id: 2>
>> f.origin.code
=> "ORD"

And for fun, check out the arrivals for the Portland airport:

>> Airport.find_by_code("PDX").arrivals
=> [#<Flight id: 1, number: "123", created_at: "2008-04-05 20:05:24", updated_at: "2008-04-06 10:22:23", meal: false, equipment: nil, origin_id: 1, destination_id: 2>, #<Flight id: 4, number: "444", created_at: "2008-04-06 14:31:39", updated_at: "2008-04-06 14:31:39", meal: false, equipment: "747", origin_id: 1, destination_id: 2>]

List Box instead of Drop-down Box

If you’d prefer to show a listbox instead of a combobox, we need to tell the browser to show more than one item at a time. To do that we need to pass two more parameters to the collection_select method:

  • An empty hash. See the Rails API docs for more info here – this hash can contain optional elements like whether to include a blank item at the top of the list, etc.
  • A hash of attributes that should be glued into the HTML <select> tag. We need to insert a size attribute with the value 3. Anything larger than 1 will end up looking like a listbox instead of a combobox.

Adding these options and playing with our <p> tags a bit means we have code that now looks like this:

  <p>Depart from:</p> 
    <%= f.collection_select :origin_id, airports, :id, :code, {}, {:size => 3}  %>

  <p>Arrive at:</p>
    <%= f.collection_select :destination_id, airports, :id, :code, {}, {:size => 3} %>
  </p>

and the form now looks like this:

You Are Now Free To Create Your Own Forms

We’ve gotten a few questions regarding the best way to handle several different models on the same form, multiples of the same model on the same form, and how to use advanced options of how to use form_for when your controller is a namespace (if your controller is within an “admin” namespace, for example). We’ll try to address those issues in subsequent postings in the very near future.

But for now, that’s it for our whirlwind tour of forms in Rails. Remember:

  • Ruby templates generate HTML. So learn HTML.
  • The params has is your bridge between your user and your application. Learn to use the log files well.
  • Follow Rails conventions to make life easy.

Questions? Drop us a comment.

Comments

Leave a response

  1. Shannon   April 11, 2008 @ 01:14 AM

    Great series, thanks!

  2. Benny   April 12, 2008 @ 04:05 PM

    (First, I want to apologize for any bad English in this comment.)

    I just have a question regarding this line: <% airports = Airport.find(:all, :order => :code) %>

    As a Rails beginner I was wondering whether it would be more Rails-like to put this line into the flights_controller like this:

  3. Benny   April 12, 2008 @ 04:06 PM

    (First, I want to apologize for any bad English in this comment.)

    I just have a question regarding this line: <% airports = Airport.find(:all, :order => :code) %>

    As a Rails beginner I was wondering whether it would be more "Rails-like" to put this line into the flights_controller like this: @airports = Airport.find(:all, :order => :code) %>

    Thanks for your great blog! It really helped me to get over a lot of things!

  4. Jeff   April 12, 2008 @ 04:19 PM

    Benny,

    You are absolutely correct. I'd recommend moving that to the controller code.

    Thanks for asking!

  5. Adam   April 20, 2008 @ 08:35 AM

    Jeff thanks a lot for this Absolute Moron's guide series! It's great to read through and get some familiarity with RoR! Great work! Cheers!

  6. Adam   April 20, 2008 @ 02:21 PM

    Jeff your series is great! I have learnt a lot today, thankyou very much!

  7. austin_web_developer   April 26, 2008 @ 05:17 AM

    Hi Jeff. Love the blog and this series in particular. I was wondering if you could do one on building forms without using a model.

  8. Adrian   April 29, 2008 @ 04:19 AM

    Jeff,

    Thanks so much for your help. This is a great blog, and has explained some concepts that my aging mind found tough to pick up in the past.

    Sorry about your 'Hawks. I share your pain, as I am Blues fan from way back. I long for the days of the Blues-Blackhawks rivalry. Roenick, Chelios, Savard, Graham, et. al. But especially the Al Secord days.

  9. Jeff   April 29, 2008 @ 02:07 PM

    @austinwebdeveloper: I will try to do that soon. Thanks for the suggestion!

    @Adrian: Savard-Larmer-Secord was the best line the Hawks had since the 1960's and haven't had one since. Loved the Blues back in the day, especially Bernie Federko and Brian Sutter.

  10. Anp   May 16, 2008 @ 07:28 AM

    Hi,

    Thanks for this blog…...... I was searching for something like this….. But I want to select an item the collection_select then pass it to another action(delete/edit) .... but I cannot pass the value to another action without submitting the form….(I use start_from_tag) .... Could you please suggest something…. I can provide with more details if needed. Thanks, Anp

  11. Seth   May 17, 2008 @ 12:08 AM

    I’m having trouble with the Airport.find_by_code ’ _ ’ snippet. I’m getting a ‘method_missing’ in the /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1483. Also, can’t seem to find any documentation in the api about a “find_by_code” method. Please help a noob!

    sc

  12. Jeff   May 18, 2008 @ 01:33 AM

    Anp: Sounds like you're trying to use an Ajax observer. See @observe_field in the Rails, it should do exactly what you’re looking for.

    Seth: Make sure you have a column named "code". @find_by_code is an example of a “dynamic finder”. In Rails, you can do find_by_column where column is any column in your table. If that doesn’t help, post something on our forum and we can take it from there.

  13. kino   May 24, 2008 @ 01:10 AM

    As is proven in the ontological manuals, the never-ending regress in the series of empirical conditions, even as this relates to reason, would be falsified.

  14. Ellroy   May 30, 2008 @ 11:17 PM

    As is proven in the ontological manuals, what we have alone been able to show is that, so far as regards the discipline of natural reason and our ideas, the noumena are just as necessary as the empirical objects in space and time, and the Categories can be treated like the discipline of natural reason.

  15. Chris Small   July 01, 2008 @ 05:06 AM

    Awesome set of posts! I like how in depth you get with explaining things – so many tutorials gloss things over with statements to the effect of “and then, by the magic of rails…” but you really explain what’s going on which is great. You’ve helped me clarify a lot. : )

    I must also say, I would love to see some stuff on use of fields_, multiple models in one form and dynamic forms. I understand you’ve already gotten requests for this sort of thing, but I suppose a little bit of reiteration doesn’t hurt.

    One more thing – when at http://www.softiesonrails.com/tags/forms the third part to the series seems to be missing. Is this some error? Also it would be nice if on each individual post you had a link to the next and previous posts in the series.

    Thanks again!

  16. cecil thornhill   July 29, 2008 @ 10:34 PM

    The tutorial is great, but leaves a bit out: Once you add all this data to the new method, you will want to see it in the index method. I have worked through most of the issues, but one has me stopped – after adding the origin and destination, I would like to show these values in the index method of flight, but for the life of me I can’t get the right way of pulling the airport.code against the flight.origin_id and flight.destination_id to work right in the index method. If possible I would love to see how this is supposed to be done (best practice). Any help would be appreciated. Also, it is worth noting that the format for display of checkboxes and buttons is different for display than it is for forms – a factor that costs be a bit of time working through. Thanks in advance for any assistance.

  17. Jeff   July 30, 2008 @ 05:59 PM

    Chris: I’ll check out the problem with the tags. Thanks for the heads up.

    Cecil: Thanks – and I’ve replied to your post in the forums here

  18. garg   August 14, 2008 @ 04:19 PM

    Is there a way to get a multi-select list box?

    Thanks.

  19. Mark   August 14, 2008 @ 06:33 PM

    Great series of tutorials! Perfect introduction for a Rails beginner as myself. Today i went through all the REST tutorials and the Forms in Rails tutorials. All went smoothly until part 5, top of the page. I’ve typed in: script/generate scaffold Airport code:string rake db:migrate And when i go to http://localhost:3000/airports all looks well. Only problem is that when i create a new airport nothing happens, no airport is added. I get this again: “Listing airports Code New airport”

    This is in my development.log though :Parameters: {“airport”=>{“code”=>”jfk”}, “commit”=>”Create”

    Am i missing something here or have i just been staring at this screen too long? :) Thanks!

  20. garg   August 14, 2008 @ 07:59 PM

    Mark > Try to restart your web server. If you are using webrick, do Ctrl+C and then run script/server again.

    Hope this helps.

  21. garg   August 14, 2008 @ 08:21 PM

    To answer my own questions:

    Multi-select listbox: {:multiple => ‘multiple’} %>

    <%= f.collection_select :destination_id, airports, :id, :code, {},
  22. Mark   August 15, 2008 @ 08:04 AM

    Hello Garg, thanks, works now:)

Comment


(won't be published)