Extending Complex Forms: generate valid dom ids

Posted by jeff

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:

References

Comments

Leave a response

  1. tomicemymaAugust 01, 2008 @ 02:13 AM

    A blog about link directories where you can read articles and find good link directories.

  2. NicoSeptember 24, 2008 @ 09:35 PM

    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!

  3. Elijah GilliamNovember 12, 2008 @ 03:45 PM

    r31rgojjpsiumvy0

Comment