
- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols

CUSTOM FORM BUILDERS 494
By default this uses the CSS style errorExplanation; you can borrow the definition from scaffold.css, write your own definition, or override the style in the generated code.
22.6Custom Form Builders
The form_for helper creates a form builder object and passes it to the block of code that constructs the form. By default, this builder is an instance of the Rails class FormBuilder (defined in the file form_helper.rb in the Action View source). However, we can also define our own form builders, letting us reduce duplication, both within and between our forms.
For example, the template for a simple product entry form might look like the following:
<% form_for :product, :url => { :action => :save } do |form| %>
<p>
<label for="product_title">Title</label><br/> <%= form.text_field 'title' %>
</p>
<p>
<label for="product_description">Description</label><br/> <%= form.text_area 'description' %>
</p>
<p>
<label for="product_image_url">Image url</label><br/> <%= form.text_field 'image_url' %>
</p>
<%= submit_tag %> <% end %>
There’s a lot of duplication in there: the stanza for each field looks about the same, and the labels for the fields duplicates the field names. If we had intelligent defaults, we could really reduce the body of our form down to something like the following.
<%= form.text_field 'title' %>
<%= form.text_area 'description' %> <%= form.text_field 'image_url' %> <%= submit_tag %>
Clearly, we need to change the HTML produced by the text_field and text_area helpers. We could do this by patching the built-in FormBuilder class, but that’s fragile. Instead, we’ll write our own subclass. Let’s call it TaggedBuilder. We’ll put it in a file called tagged_builder.rb in the app/helpers directory. Let’s start by rewriting the text_field method. We want it to create a label and an input area, all wrapped in a paragraph tag. It could look something like this.
Report erratum

CUSTOM FORM BUILDERS 495
class TaggedBuilder < ActionView::Helpers::FormBuilder
#Generate something like:
#<p>
#<label for="product_description">Description</label><br/>
# <%= form.text_area 'description' %>
#</p>
def text_field(label, *args) @template.content_tag("p" ,
@template.content_tag("label" , label.to_s.humanize,
:for => "#{@object_name}_#{label}" ) +
"<br/>" + super)
end end
This code uses a couple of instance variables that are set up by the base class, FormBuilder. The instance variable @template gives us access to existing helper methods. We use it to invoke content_tag, a helper that creates a tag pair containing content. We also use the parent class’s instance variable @object_name, which is the name of the Active Record object passed to form_for. Also notice that at the end we call super. This invokes the original version of the text_field method, which in turn returns the <input> tag for this field.
The result of all this is a string containing the HTML for a single field. For the title attribute of a product object, it would look something like the following (which has been reformatted to fit the page).
<p><label for="product_title" >Title</label><br/>
<input id="product_title" name="product[title]" size="30" type="text" />
</p>
Now we have to define text_area.
def text_area(label, *args) @template.content_tag("p" ,
@template.content_tag("label" , label.to_s.humanize,
:for => "#{@object_name}_#{label}" ) +
"<br/>" + super)
end
Hmmm.... Apart from the method name, it’s identical to the text_field code. Let’s eliminate that duplication. First, we’ll write a class method in TaggedBuilder that uses the Ruby define_method function to dynamically create new tag helper methods.
Report erratum

CUSTOM FORM BUILDERS 496
Download e1/views/app/helpers/tagged_builder.rb
def self.create_tagged_field(method_name) define_method(method_name) do |label, *args|
@template.content_tag("p" , @template.content_tag("label" ,
label.to_s.humanize,
:for => "#{@object_name}_#{label}" ) +
"<br/>" + super)
end end
We could then call this method twice in our class definition, once to create a text_field helper and again to create a text_area helper.
create_tagged_field(:text_field) create_tagged_field(:text_area)
But even this contains duplication. We could use a loop instead.
[:text_field, :text_area ].each do |name| create_tagged_field(name)
end
We can do even better. The base FormBuilder class defines a collection called field_helpers—a list of the names of all the helpers it defines. Using this our final helper class looks like this.
Download e1/views/app/helpers/tagged_builder.rb
class TaggedBuilder < ActionView::Helpers::FormBuilder
#<p>
#<label for="product_description">Description</label><br/>
#<%= form.text_area 'description' %>
#</p>
def self.create_tagged_field(method_name) define_method(method_name) do |label, *args|
@template.content_tag("p" , @template.content_tag("label" ,
label.to_s.humanize,
:for => "#{@object_name}_#{label}" ) +
"<br/>" + super)
end end
field_helpers.each do |name| create_tagged_field(name)
end
end
Report erratum

CUSTOM FORM BUILDERS 497
How do we get Rails to use our shiny new form builder? We simply add a
:builder parameter to form_for.
Download e1/views/app/views/builder/new.rhtml |
|
|
<% form_for :product, |
:url => { :action => :save }, :builder => TaggedBuilder do |form| %> |
|
<%= form.text_field |
'title' %> |
|
<%= form.text_area |
'description' |
%> |
<%= form.text_field |
'image_url' |
%> |
<%= submit_tag %> |
|
|
<% end %> |
|
|
If we’re planning to use our new builder in multiple forms, we might want to define a helper method that does the same as form_for but that adds the builder parameter automatically. Because it’s a regular helper, we can put it in helpers/application_helper.rb (if we want to make it global) or in a specific controller’s helper file.
Ideally, the helper would look like this.
# DOES NOT WORK
def tagged_form_for(name, options, &block)
options = options.merge(:builder => TaggedBuilder) form_for(name, options, &block)
end
However, form_for has a variable-length parameter list—it takes an optional second argument containing the model object. We need to account for this, making our final helper somewhat more complex.
Download e1/views/app/helpers/builder_helper.rb
module BuilderHelper
def tagged_form_for(name, *args, &block)
options = args.last.is_a?(Hash) ? args.pop : {} options = options.merge(:builder => TaggedBuilder) args = (args << options)
form_for(name, *args, &block) end
end
Our final view file is now pretty elegant.
Download e1/views/app/views/builder/new_with_helper.rhtml
<% tagged_form_for :product, :url => { :action => :save } do |form| %> <%= form.text_field 'title' %>
<%= form.text_area 'description' %> <%= form.text_field 'image_url' %> <%= submit_tag %>
<% end %>
Form builders are one of the unsung heroes of Rails: you can use them to establish a consistent and DRY look and feel across your application, and you can share them between applications to impose a company-wide standard for
Report erratum