Dynamic forms with the repetition model
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 create
s 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 Option
s 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 Question
s and Option
s 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.