Dynamic forms with the repetition model

Manfred Stienstra

I’m sure we’ve all been there; trying to save a list of associated objects together with the instance they belong to. Fortunately with the right tools this can be really simple.

Let’s assume we have a question in a questionnaire and need to specify an arbitrary number of options to this question. We could construct the following database schema.

create_table :questions do |t|
  t.column :stem,        :text
end

create_table :options do |t|
  t.column :question_id, :integer
  t.column :label,       :text
  t.column :feedback,    :text
  t.column :correct,     :boolean, :default => false
end

There are basically three ways I know about to assign multiple options to a question from one page. The first option is to create a Question instance in the database before rendering the form so we can use remote_form to send an Ajax request and directly associate the Option instance. An advantage of this solution is that all data is directly saved to the database, which makes losing any data less likely. Another advantage is that the functionality is spread nicely over the responsible controllers. First you perform a create on the QuestionsController, after that you perform multiple creates on the OptionsController. The disadvantage is that you have to create a Question with default values or by circumventing the initial validation. This could generate some ‘blank’ Question instances in the database which you will have to garbage collect.

The second option is a slight variation on the first option. Instead of creating a Question to link the Options to you keep all the generated Option id’s in the session and link them to the Question after validation. This solution also benefits from nice separation of concern in the controllers, but on the downside it also leaves you with unlinked Option objects you will have to garbage collect.

The third option is to keep all the information about the Questions and Options in the DOM tree of your from and post it all at once. This used to be hard, requiring a lot of custom JavaScript and browser tricks. Fortunately WHATWG is here to help; Web Forms 2.0 defines a repetition model for repeating form controls. Browser support for these interaction models is years away (except for Opera 9, which has an experimental implementation), that’s why there’s a JavaScript library to accelerate the adaptation.

It’s really easy to use the library in your Rails application. Download the repetitionmodel library, extract the zipfile and put the javascript files in public/javascripts. When you’re done with that we can get back to coding our application. Include the JavaScript file somewhere in your views.

<%= javascript_include_tag 'repetition-model-p' %>

We just mentioned that we want to send the entire form at once, we can use validates_associated to make sure the Question is never saved when the options aren’t valid. This allows you to easily validate all the information from the form at once. The models to go with the database look something like this.

class Question < ActiveRecord::Base
  has_many :options, :dependent => :delete
  validates_presence_of :stem
  validates_associated :options
end

class Option < ActiveRecord::Base
  belongs_to :question
  validates_presence_of :label
end

Build the new and edit forms for the QuestionController like you would normally do. A the bottom of the form we will add the HTML to edit the options.

<h2>Options</h2>
<ol id="options">
  <%= render :partial => 'options/option', :collection => @question.options %>
  <%= render :partial => 'options/option', :object => Option.new,
    :locals => {:option_counter => '[option]'} %>
</ol>
<p><button type="add" template="option">New option</button></p>

The first render is for all the existing options, the second render is for what the repetition model calls a template, the template should always be the last in the list of controls.

In app/views/options/_option.rhtml we put the following. This partial doubles as a template for new controls and as a partial for existing options in the database.

<% if option_counter == '[option]' -%>
<li id="option" repeat="template" repeat-start="0">
<% else -%>
<li repeat="<%= option_counter %>">
<% end -%>
  <div>
    <div><label>Label</label></div>
    <%= text_area_tag "options[#{option_counter}][label]", option.label,
      :rows => 1, :cols => 40 %>
  </div>
  <div>
    <div><label>Feedback</label></div>
    <%= text_area_tag "options[#{option_counter}][feedback]", option.feedback,
      :rows => 1, :cols => 40 %>
  </div>
  <div>
    <label><%= radio_button_tag "options[correct]", option_counter, option.correct?,
      :id => "options_correct_#{option_counter}" %> Correct</label>
  </div>
  <div>
    <button type="remove">Delete option</button>
  </div>
</li>

In the HTML you see some extra attributes used by the JavaScript to determine what to do with them. When the partial is rendered with an new Option, the template is flagged by setting the repeat attribute to template. The id of this element is used as a handle for our controls, in our case this is ‘option’. The repeat-start attribute tells the javascript how many empty controls to generate initially from the template, we don’t want any so we’ve set it to 0. Note that we explicitly set the option_counter to ‘[option]’, this is the variable notation for the repetition model. When a new control is instanciated from the template this variable is replaced by the the index of the new control. The first control gets index 0, the second gets index 1 and so forth.

When the partial is rendered with a collection, the magic variable option_counter is set to the index of the collection every time the partial is rendered. We use this index to set the repeat attribute of the list item, this signals to the JavaScript that this is an already instantiated control. The JavaScript will start counting from the largest index when it instantiates a new control.

Finally we want the user to add and remove option controls in our page, this is done with the ‘New option’ and ‘Delete option’ buttons. Their type attribute signals what we want the JavaScript to do with the repetition blocks. In case of the ‘New option’ button the template attribute tells the JavaScript which template to instantiate.

In addition to managing the controls and template in the DOM tree, the JavaScript also takes care of disabling buttons on appropriate times and setting the CSS display property of the template to none. They really thought of everything.

The advantage of this solution is that you can use all the standard Rails tricks to keep your database clean. The biggest disadvantage is that the create method in your QuestionsController becomes a lot more complex.

Let’s hope native browser implementations follow quickly.