While Rails scaffolding is a convenient way to get started, you may have noticed that it’s pretty repetitive and not especially attractive. Some of this can be fixed with judicious use of CSS, but Rails supports more permanent fixes through the use of form builders. Creating form builders is an opportunity to define how your data will be presented to users and how they’ll interact with that data. Form builders also let you create abstractions that keep programmers out of the visual details of your application while still giving them full access to views.
The basic concepts behind form builders are simple, though you can use them to create complex and intricate components. You can use form builders in multiple ways, starting from simple wrapping of your own special types and developing through more complex ways to change the ways forms are written.
You can also combine form builders with Ruby metaprogramming to create your own terse yet descriptive syntaxes for creating forms, but metaprogramming is way beyond the scope of this book. If you encounter an application with view code that looks nothing like you expected, though, that may be what’s going on.
Chapter 6 showed how Rails supported a variety of data types by default, including a more complicated (if not very user-friendly) set of controls for entering dates. While the built-in set of widgets is helpful, you’re definitely not limited to it. You can build reusable form components that match your needs more precisely.
This can be very useful when you have components that take the same limited set of values. Chapter 6 showed a helper method for creating drop-down lists or radio buttons depending on the number of choices, culminating in Example 6-10, which is repeated here as Example 8-1.
Example 8-1. A helper method for creating select lists
def button_select(model_name, target_property, button_source) list = button_source.sort if list.length < 4 list.collect do |item| radio_button(model_name, target_property, item[1]) + h(item[0]) end.join('<br />') else select(model_name, target_property, list) end end
Rather than create a generic helper method whose focus is on the
kind of HTML it generates, it can be more appealing to create a form
builder method whose focus is on data that’s appropriate for a given
model. Linking the HTML specifically to a given model makes it vastly
easier to keep interfaces consistent. Example 8-2, included in
ch08/guestbook008, shows a form
builder method, country_select
,
that is designed specifically for use with the :country
field.
Example 8-2. A form builder providing a method more tightly bound to the expectations of the country field
class TidyFormBuilder < ActionView::Helpers::FormBuilder # our country_select calls the default select helper with the # choices already filled in def country_select(method, options={}, html_options={}) select(method, [['Canada', 'Canada'], ['Mexico', 'Mexico'], ['United Kingdom', 'UK'], ['United States of America', 'USA']], options, html_options) end end
Note that form builders, which go in the app/helpers directory, all inherit from the ActionView::Helpers::FormBuilder
class. (Rails has its own country_select
method with a much larger
selection of countries, but this demonstration overrides it.) The
methods inside the class will then be made available to views that
specify that they want to use this helper.
The country_select
method is
built using the select
helper method already explored in Chapter 6. It takes a method
parameter and options
and html_options
, like the select
method does. What does the method
parameter do? You probably think of
the method
as the field—:name
, for example. You can see how much
more tightly bound country_select
is to :country
—it wouldn’t be of
much use for any other field, unless you have, say, different kinds of
fields expecting the same list of countries. The result is a select
field seeded with the choices you’ve
deemed acceptable for country.
Note that the options
and
html_options
arguments are simply
passed through. Preserving them offers developers more flexibility
when they go to apply your form builder in different
situations.
Calling the form builder requires two things. First, the view
has to reference the TidyFormBuilder
, and then it has to
actually call country_select
.
Unlike helper classes, where a naming convention is enough to connect
supporting code with the view, form builders require an explicit call.
(You will likely use the same builder for multiple views in any case,
as :country
might turn up in a lot
of different contexts.)
As was the case for the multipart form, calling the builder
means using an expanded declaration on form_for
, this time adding a :builder
parameter:
<% form_for(:person, @person,
:url => person_path(@person),
:html => { :multipart => true,
:method => (@person.new_record? ? :post : :put)},
:builder => TidyFormBuilder) do |f| %>
Rails will know to look for /app/helpers/TidyFormBuilder
. Actually,
calling the method is pretty simple. Just replace:
<p> <%= f.label :country %><br /> <%= f.select (:country, [ ['Canada', 'Canada'], ['Mexico', 'Mexico'], ['United Kingdom', 'UK'], ['United States of America', 'USA'] ]) %> </p>
with:
<p> <%= f.label :country %><br /> <%= f.country_select :country %> </p>
The results will be identical, but the logic around the country
object is much better encapsulated,
and just plain easier to use, in the builder version.
The Rails helper methods are certainly useful, but they tend to map directly to HTML markup. When you have multiple related markup components for a single field, code can start to feel messy very quickly. That’s true even when they’re as simple as an input field with a label, like this from the scaffolding:
<p> <%= f.label :name %><br /> <%= f.text_field :name %> </p>
Multiply that by a hundred fields, and there’s a lot of repetitive code around. Remember, the Rails mantra is “Don’t Repeat Yourself” (DRY), and there’s a huge opportunity to avoid repetition here.
Although it’s kind of a separate task from the country selector,
this can also happen easily inside of the TidyFormBuilder
, as shown in ch08/guestbook008. In fact, it’s easy for
it to take place there because methods in the builder can use the same
names as the helper methods and subclass them, adding the extra
functionality needed to simplify the view code. About the only tricky
part is making sure that your subclassed methods use the same
signature—list of parameters—as the originals, which just means
checking the Rails API documentation:
def text_field(method, options={}) ... end
The text_field
method takes a method
parameter. The options
array is the
usual set of options. Once the signature is set up, the single line of
code inside combines a label with a call to the original method to
create a return value:
def text_field(method, options={})
label_for(method, options) + super(method, options)
end
Calling super
, in the second
half of this line, means to call the original text_field
method, which gets passed the
method
and options
objects. The first half of the line
calls another method, however, adding the label. The label_for
method is declared at the end of
the TidyFormBuilder
class and is
private
, as it is for internal use
only:
private def label_for(method, options={}) label(:label || method) + "<br />" end
The label
method is the same
as usual and is concatenated to a <br
/>
tag, but there’s something tricky going on in the
arguments:
(:label || method)
This looks for an option named :label
, letting you specify label text for
the field through a :label
parameter. If there isn’t a :label
,
the ||
will fall through to method
, which will create a label with the
default—the internal name of the field.
If you wanted to make this method more like its cousins in the Rails framework itself, you’d add a method to the line that creates the label:
label(options.delete
(:label) || method) + "<br />"
Accessing the :label
value
through delete
seems strange, but delete
does two things: it removes the
:label
parameter from the
options
array, which will keep it
from passing through to the super
call, and it also returns the :label
parameter’s value, if there is
one.
The call to create a field is now much simpler:
<%= f.text_field :name %>
The other methods, with more complex signatures, need a bit more code, but it’s the same basic logic, as these two demonstrate:
def datetime_select(method, options = {}, html_options = {}) label_for(method, options) + super(method, options, html_options) end def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") label_for(method, options) + super(method, options, checked_value, unchecked_value) end
And again, the calls to create a select list and a checkbox become simpler:
<%= f.check_box :can_send_email %> <%= f.datetime_select :favorite_time %>
There’s one last bit to notice. Remember how country_select
calls the select
method? It now calls the method that
provides the label. That means that you can simplify:
<p> <%= f.label :country %><br /> <%= f.country_select :country %> </p>
to:
<p> <%= f.country_select :country %> </p>
The next step will reduce this even further, while making it easier to style and manipulate the resulting HTML.
All of those <p>
and
</p>
tags are calling out for
simplification, but there’s another opportunity here: to add
additional information to the form that will help users fill it out
properly. The WrappingTidyBuilder
,
included in ch08/guestbook010,
builds on the prior TidyBuilder
,
supporting its country_select
method and its methods for providing labels. It also, however, takes
advantage of the work it’s putting into wrapping to add some extra
information to fields that are required. This requires a few extra
components:
A wrap_field
method that
puts the opening and closing tags around the label and form
fields
Calls to wrap_field
from
the other methods
A bit of extra code in the label
method that adds a textual
indicator that a field is required
Support for the new wrapper in a CSS stylesheet used for pages built with these methods
Linking that CSS stylesheet to your application through an addition to the layout file
The :required
option is
specified in calls to the form builder’s methods, if desired:
<%= f.text_field :name, :required => true %> <%= f.password_field :secret, :required => true %> <%= f.country_select :country, :required => true %>
:required
, in this code, is
only about how the field should be presented. Specifying whether a
field should genuinely be required is better done in the model
validation described in Chapter 7.
The wrap_field
method, like the label_for
method, comes after private
in the code, making it callable only
within the class or subclasses. It’s not very complicated, choosing
what value to use for the class attribute based on the contents of the :required
option:
def wrap_field(text, options={}) field_class = "field" if options[:required] field_class = "field required" end "<div class='#{field_class}'>" + text + "</div>" end
By default, class
, which
gives CSS stylesheets a hook for formatting the div
, will just contain “field.” It’s a form
field. If :required
is true
, however, it will have the value “field
required.” The class
attribute can
contain multiple values separated by spaces, so this means that the
stylesheet can format this div
as
both a form field and as required.
The other methods need to call wrap_field
, which makes them slightly more
complicated. Therefore, the following:
def text_field(method, options={}) label_for(method, options) + super(method, options) end
grows to become this:
def text_field(method, options={}) wrap_field(label_for(method, options) + super(method, options), options) end
Looking through the parentheses, this means that wrap_field
gets called with the text
generated by the older methods, along with the options that it also
needs to explore.
This wrapping happens for all of the public methods in WrappingTidyBuilder
, with one important
exception: country_select
. Why?
Because country_select
already
calls select
, which will do the
wrapping for it.
Connecting a field option to CSS styling is a good idea, but
there’s one problem: not every browser uses CSS. Remember Lynx, the
text-only web browser? It’s still out there, and so are a lot of
different browsers that don’t use CSS. Some are screen readers, others
are simplified browsers for cell phones and other devices. To address
that possibility, modifying label_for
will add an asterisk to required
fields, using the same logic that wrap_field
had used:
def label_for(method, options={}) extra = "" if options[:required] extra = " <span class='required_mark'>*</span>" end label(options.delete(:label) || method) + extra + "<br />" end
If the :required
option is
set to true
, this means that the
label will have an extra <span
class='required_mark'>*</span>
appended after the
label and before the <br />
break between the label
and the field.
The last piece needed is a stylesheet. The stylesheet itself will go into the public/stylesheets/ directory, and here is called public.css. From there, it will be accessible to your application through the web server at /stylesheets/public.css.
As you can see, though this isn’t a book on CSS, four styles are
defined. One is for the field, another for the label inside the field,
another for the asterisk in the required_mark
-classed span, and a last one
is for the fields marked required
:
/* styles for our forms */ div.field { margin-top: 0.5em; margin-bottom: 0.5em; padding-left: 10px; } div.field label { font-weight: bold; } div.field span.required_mark { font-weight: bold; color: red; } /* draw attention to required fields */ div.required { padding-left: 6px; border-left: 4px solid #dd0; }
One last piece is needed—the application needs to reference this
new stylesheet. The easiest way to do this is to add a line to the
/app/views/layouts/people.html.erb file,
using the stylesheet_link_tag
helper method:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>People: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
<%= stylesheet_link_tag 'public' %>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>
So, what does all this look like? Figure 8-7 gives you a sense of what’s happened. Note the bars along the left edge of the required fields (yellow on the screen) and the red asterisks after their labels.
The first time through, this seems like a lot of work. And the
first time through, it is. The good news, however, is that once you’ve
done this, all that work is easy to reuse. You can change the
stylesheet without having to go back to the layout. You can change the
wrap_field
method to do whatever
you like. Once the infrastructure is built, it’s much easier to change
the details or to assign different details to different people working
on a project, without fear of collision.