Creating a Web Form with Form Helpers

Problem

Contributed by: Diego Scataglini

You need to create a typical sign-up form, perhaps for a company newsletter. You want to validate all required fields as well as make sure that users accept the terms and conditions.

Solution

Creating web forms is probably the most common task in web development. For this example, assume you have a Rails application created with the following table structure:

class CreateSignups < ActiveRecord::Migration
 def self.up
 create_table :signups do |t|
 t.column :name, :string
 t.column :email, :string
 t.column :dob, :date
 t.column :country, :string
 t.column :terms, :integer
 t.column :interests, :string
 t.column :created_at, :datetime
 end
 end
 def self.down
 drop_table :signups
 end end

Create a corresponding model and controller:

$ ruby script/generate model signup

$ ruby script/generate controller signups index

Now add some validations to the Signup model:

app/models/signup.rb:

class Signup < ActiveRecord::Base
 validates_presence_of :name, :country
 validates_uniqueness_of :email
 validates_confirmation_of :email
 validates_format_of :email, 
 :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
 validates_acceptance_of :terms, 
 :message => "Must accept the Terms and Conditions"
 serialize :interests
 def validate_on_create(today = Date::today)
 if dob > Date.new(today.year - 18, today.month, today.day)
 errors.add("dob", "You must be at least 18 years old.")
 end
 end end

Next, add the following index method to your Signups controller:

app/controllers/signups.rb:

class SignupsController < ApplicationController
 def index
 @signup = Signup.new(params[:signup])
 @signup.save if request.post?
 end end

Finally, create the index.rhtml view:

app/views/signups/index.rhtml:

<%= content_tag "div", "Thank you for registering for our newsletter",
 :class => "success" unless @signup.new_record? %>
<%= error_messages_for :signup %>
<% form_for :signup, @signup do |f| %>
 <label for="signup_name">Full name:</label>
 <%= f.text_field :name %><br />
 <label for="signup_email">Email:</label>
 <%= f.text_field :email %><br />
 <label for="signup_email_confirmation">Confirm Email:</label>
 <%= f.text_field :email_confirmation %><br />
 <label for="signup_dob">Date of Birth:</label>
 <%= f.date_select :dob, :order => [:day, :month, :year],
 :start_year => (Time.now - 18.years).year,
 :end_year => 1930 %><br />
 <label for="signup_country">Country:</label>
 <%= f.country_select :country, ["United States", "Canada"] %><br />
 <label for="signup_terms">I Accept the Terms &amp; Conditions:</label>
 <%= f.check_box :terms %><BR clear=left>
 <h3>My interests include:</h3>
 <% ["Swimming", "Jogging", "Tennis"].each do |interest|%>
 <label><%= interest %></label>
 <%= check_box_tag "signup[interests][]", interest,
 (params[:signup] && params[:signup][:interests]) ?
 params[:signup][:interests].include?(interest) : false %>
 <br />
 <% end %>
 <%= submit_tag "Signup", :style => "margin-left: 26ex;" %>
<% end if @signup.new_record? %>

Optionally, for some presentational style, add the following lines to your scaffold.css, and then you're done:

public/stylesheets/scaffold.css:

label {
 display: block; 
 float: left; 
 width: 25ex;
 text-align: right; 
 padding-right: 1ex;
}
.success {
 border: solid 4px #99f;
 background-color: #FFF;
 padding: 10px; 
 text-align: center;
 font-weight: bold; 
 font-size: 1.2em; 
 width: 400px;
}

Discussion

Rails gives you the tools to make even a tedious task, such as creating a form and handling field validation and state, fun. Action View has form helpers for just about any occasion, and creating ad hoc helpers is a breeze. Once you're familiar with Active Record's validations module, creating a form becomes child's play.

shows the solution's sign-up form.

Figure 5-8. A sign-up form containing elements generated using form helpers

The solution uses form_for, which takes a symbol as the first parameter. This symbol is used by Rails as the object name and will be yielded to the block. The f in f.text_field represents the connection between the helper and the object model to which it refers. The second parameter is an instance variable that is prepopulated by the index action in the controller and is used to keep state between page submissions.

Any helper that takes an object and a method as the first parameters can be used in conjunction with the form_for helper.

Action View provides you with date_select and datetime_select helpers, among others, to handle dates and times. These helpers are very easy to configure. You can hide and reorder the parts of the date by using the :order parameter. For example:

date_select("user", "birthday", :order => [:month, :day])

The framework also collects useful information, such as lists of all countries and time zones, and makes them available as helpers as well as constants (e.g., country_select, country_options_for_select, time_zone_options_for_select, time_zone_select).

The validates_confirmation_of class method is worth noting. This method handles confirmation validation as long as the form includes a confirmation field. The solution requires the user to confirm her email address, using the form's email_confirmation field. If you need to confirm a password field, you can add a password_confirmation field as well.

For the interests field, you need to provide multiple checkboxes for different interests. The user can check any combination of these boxes; the application needs to collect the results and serialize them into a single field. Therefore, you can't use the facility offered by form_for. You indicate that a field will repeat itself and allow multiple values by appending [] at the end of the field's name. Even though the solution uses form_for to create the form, you can still mix and match helpers that don't quite fit the formula.

The solution used object introspection to detect whether to show a confirmation message or the sign-up form to the user. Although introspection is a clever way to show a confirmation page, it is preferable to redirect to a different action. Here's how to fix that:

class SignupsController < ApplicationController
 def index
 @signup = Signup.new(params[:signup])
 if request.post? && @signup.save
 flash[:notice] = "Thank you for registering for our newletter"
 redirect_to "/"
 end
 end end

See Also

  • " for more on view helpers for formatting output