Dynamic forms with the repetition model

Manfred Stienstra, 25 Jan 2007, 12:06 in ruby on rails, javascript, and web (edit).

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.

6 comments

Embedding applications with Javascript

Manfred Stienstra, 06 Jun 2006, 10:53 in javascript and web (edit).

RightCart is a cool service that allows you to embed a shopping cart into your webpage. It looks great, but I think it should be a little bit easier to set up.

When you create an account, you get a small piece of HTML to add to your web page (I’ve added formatting for clarity):

<div class='rightcart_div'>
  <script type='text/javascript'>rightcart_pid='1'</script>
  <script type='text/javascript' src='http://rightcart.com/static/rightcart_display.js'></script>
</div>

Above the code is the following text:

“Simply add the three lines of code below to your web page and your RightCart will appear on your site. It’s that easy.”

But why three lines? Let’s look at the information this application needs. I see a div with a the class ‘rightcart_div’, followed by a variable ‘rightcart_pid’ and after that an URL for a piece of JavaScript. In Rhubarb we do the same with a single line of code:

<script type="text/javascript" src="http://rhubarb/inline/58"></script>

So why is this shorter? We don’t need a named div because our javascript adds it to the page. We don’t need to set a user id of some sort because it’s in the URL. That leaves a single script tag.

Even better, the full code contains a link to a full-page version of the same content for when JavaScript is not available:

<script type="text/javascript" src="http://rhubarb.fngtps.com/inline/58"></script> 
<noscript>
  <p><a href="http://rhubarb.fngtps.com/58">Aksie!</a></p>
</noscript>

It can be dangerous to add HTML to other pages like this. The injected HTML could end up in biggest case of tagsoup you’ve ever seen, completely breaking your layout.

That’s why we offer four different ways to publish an appeal with Rhubarb: a regular page, embedded on another page inside an iframe, embedded inline and embedded with a nice lightbox effect.

This way you can choose the delivery type that best fits your site while keeping everything accessible.

No comments yet

Rails plugins released

Manfred Stienstra, 09 Feb 2006, 11:55 in ruby on rails and javascript (edit).

A few months back we decided to release the inject plugin, however we didn’t post it to the Rails wiki because we were still reorganizing our domain structure and didn’t have anonymous Subversion access to our repositories.

For now we have two plugins in the trunk, but more might follow in the future.

Inject

The inject plugin allows you to do cross-domain AJAX-like http calls. You can find (a little bit) more information on the Rails wiki entry for Inject.

Url for domain

Url for domain adds :subdomain support to the url_for method. Actually it adds this to a the url rewriter code, so you can use this options on every helper/method with url_for-like syntax. Again, more information can be found on the Rails wiki entry for Url for domain

Have fun with the plugins and lets us know if you have any problems. We have a little project coming up which uses both these plugins, so keep an eye out for that.

2 comments

Inject me

Manfred Stienstra, 30 Nov 2005, 12:54 in ruby on rails and javascript (edit).

A month or so ago I found myself staring at my screen, I had done something stupid. Almost the entire application was finished, but when I tested it on another domain nothing worked. When I started exploring the Firefox javascript window and pasted some errors in Google, I found out I couldn’t do cross-domain XMLHttpRequests. I looked at Thijs, my boss / colleague / friend, and said: ‘We have a little problem.’ After some looking around we found out that Julien Lamarre had solved our problem.

In order to fit this solution into our Rails app, I wrote inject.js and some helpers. When plugin support came out I converted my solution to a plugin, and after some testing it is ready for the world. You can download it here.

Some comments are probably in place. The Inject.Request object doesn’t work exactly like the Ajax.Request object as it always evaluates the response from the server and doesn’t support the same arguments. Please take a look at the code, because it’s the best documentation right now.

The general idea behind the javascript is that it creates an invisible div element in the body element of the page and injects script tags into this div. The URL’s in the src attribute of the script tags are actually calls to Rails controllers and actions. You will have to return javascript from the actions if you want to change the state of the page, play nice and return a ‘text/javascript’ content-type when you do this.

4 comments