A few weeks ago I was hired to build a simple event registration form. The requirements were that users needed to be able to:
- Register multiple attendees for the same organization in one step
- Specify a meal preference for each attendee, using radio buttons
Since I had recently bought Advanced Rails Recipes and watched the Ryan Bates’ excellent Railscast on complex forms I thought I was all set to go.
I followed Ryan’s instructions exactly and within a few minutes I was up and running with the javascript additions, skinny controllers and fat models and life was good. I fired up my browser, added a few attendee subforms and went to set their meal preference with the radio button when, to my dismay, clicking on any attendee’s meal preference set the meal preference for the first! Not good.
Whenever these things happen I run the page through the w3c html validator to make sure that I’ve got valid HTML. When I ran it through, it gave me several errors – I had lots of repeated ids. Why? If you follow the ARR/railscast example, all of the new subforms will have the same id. In reality, there are 2 things that need to happen:
- when the page is loaded Rails needs to set an :index on all of the fields
- when the form is added dynamically via javascript, the javascript needs to insert the correct id
Do your homework
ARR prohibits people from reproducing their code from their tutorials without permission (which I didn’t obtain). So this blog post won’t make much sense unless you read Advanced Rails Recipes and/or watch Railscast on complex forms – both of which I highly recommend. Once you have a working example of that, the rest of this post will make sense.
The setup
For this example, I’ll use the following classes:
1 2 3 4 5 6 7 |
class Registration < ActiveRecord::Base has_many :attendees end class Attendee < ActiveRecord::Base belongs_to :registration end |
The javascript
So let’s get started. First, we need to create a javascript method that will enable us to increment the index value every time – I decided to go with a generic one so that I could reuse it across my app, and have mutliple subfrms of different types on the same page but still be dry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var Subform = Class.create({ lineIndex: 1, parentElement: "", initialize: function(rawHTML, lineIndex, parentElement) { this.rawHTML = rawHTML; this.lineIndex = lineIndex; this.parentElement = parentElement; }, parsedHTML: function() { return this.rawHTML.replace(/INDEX/g, this.lineIndex++); }, add: function() { new Insertion.Bottom($(this.parentElement), this.parsedHTML()); } }); |
Next, we need to set the index when the page loads. I accomplished that like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# /app/views/layouts/application.html.erb <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title><%= yield :title %></title> <%= javascript_include_tag :defaults, :cache => 'defaults' %> <%= yield :head %> </head> <body> <%= yield %> </body> </html> # /app/views/registrations/_form.html.erb <%- content_for :head do -%> <script type="text/javascript" charset="utf-8"> //<![CDATA[ attendeeForm = new Subform('<%= escape_javascript(render(:partial => "attendee", :object => Attendee.new)) %>',<%= @registration.attendees.length %>,'attendees'); //]]> </script> <%- end -%> |
In Ryan’s original recipe he creates a rails helper to create the add link. Now that we’ve written the javscript ourselves we no longer need the helper – our “add” link now look like this:
1 |
<%= link_to_function 'Add Attendee', "attendeeForm.add()" %> |
The partial
We need to add the index to all of the form helpers, which will require some work. In addition, we need to make sure that all of the radio buttons have unique ids and that all of the labels have ids that match up with the radio buttons. So here’s what my partial looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<div class="attendee"><%- index ||= "INDEX" new_or_existing = attendee.new_record? ? 'new' : 'existing' id_or_index = attendee.new_record? ? index : attendee.id prefix = "registration[#{new_or_existing}_attendee_attributes][]" -%> <% fields_for prefix, attendee do |attendee_form| -%> <p><%= attendee_form.label :email, nil, :index => id_or_index %> <%= attendee_form.text_field :email, :index => id_or_index %> </p> <%- MealPreference.find(:all).each do |preference| -%> <p> <%- radio_id = "registration_#{new_or_existing}_attendee_attributes_#{id_or_index}_meal_preference_#{preference.name}" -%> <%= attendee_form.radio_button :meal_preference, preference.name, :id => radio_id, :index => id_or_index %> <%= content_tag :label, preference.name, :class => "radio", :for => radio_id %> </p> <%- end -%> <%= content_tag :p, link_to_function("Remove this attendee", "if(confirm('Are you sure?')){$(this).up('.attendee').remove()}") %> <% end -%> </div> |
What just happened? I added an index to every field. For existing records, this index will correspond to the attendee.id. For new records, it will be the string “INDEX”. If you recall from the javascript above the parsedHTML function replaced the word INDEX with the correct numeric index.
NOTE: getting correct radio_button ids requires my label_with_index plugin if you are running gem rails, but it looks like radio buttons create correct ids in edge.
The form
This means, however, that you need to pass an index into the partial for existing records. In my form, I’ve got the following:
1 2 3 4 5 6 7 8 9 |
<h3>Attendees</h3> <div id="attendees"> <%- @registration.attendees.each_with_index do |attendee, index| -%> <%= render :partial => "attendee", :object => attendee, :locals => {:index => index} %> <%- end -%> </div> <p id="add-attendee"> <%= link_to_function 'Add Attendee', "attendeeForm.add()" %> </p> |
Note the use of each_with_index so that we can pass the correct index in. “But wait!” you say, “What if the attendee is an existing record? Won’t that mess this up?” Fear not – in the partial we first check whether it’s a new record, and only use the index if it’s a new record.
The model
Making the changes in the model is trivial. The existing_attributes stay exactly the same – but we have to make one small change to the new attributes:
Old:
1 2 3 4 5 6 7 8 |
# app/models/registration.rb # add all new attendees def new_attendee_attributes=(attendee_attributes) attendee_attributes.each do |attributes| attendees.build(attributes) end end |
Changes to:
1 2 3 4 5 6 7 8 |
# app/models/registration.rb # add all new attendees def new_attendee_attributes=(attendee_attributes) attendee_attributes.each do |index, attributes| attendees.build(attributes) end end |
And voila! Correct dom ids and reusable javascript.
Credits
While I wrote the javascript in that example I reconstructed it from another javascript snippet I had seen (maybe on a railscast, or in the source code for some other rails app like Basecamp or Blinksale). If that looks like your javascript, please contact me at jeff at zilkey . com – I can’t find the original source right now. Other credits include:
A blog about link directories where you can read articles and find good link directories.
This fix is so timely, I’m having a lot of trouble on this one because I’ve been a Rails guy from the start and my javascript is practically nonexistent.
Anyway, thanks and more power!
r31rgojjpsiumvy0