Standardizing Your Look with Form Builders

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.

Note

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.

Supporting Your Own Field Types

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

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.

Adding Automation

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.

Note

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.

Integrating Form Builders and Styles

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 :required option specified in calls from the view

  • 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.

Extra formatting created through a form builder and CSS

Figure 8-7. Extra formatting created through a form builder and CSS

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset