3.3. Refactoring Forms Using A FormBuilder

Take another look at the code in that form view in the previous section. One thing you should notice is that the pattern of having a table row where the first cell is a caption and the second cell is a form element is repeated over and over. Not only does that make for a lot of unnecessary and error-prone repetition, but all the extra elements make for a lot of clutter and hard-to-read code.

I have to say, I'm familiar with a number of web frameworks that depend on some kind of code or HTML template, and the problem with nearly all of them is that complex code in the view or template becomes difficult to lay out, read, and maintain. Part of this is due to the fact that the code and the HTML have two distinct, intertwined structures, each with its own indentation and layout needs, and these two structures work together only barely. Other frameworks depend on building all the HTML tags programmatically, which can also be difficult to manage in complex cases.

One of the strengths of Ruby on Rails, especially when compared to systems which came before, is an awareness of this problem of structuring views. Rails provides a number of ways to move complex, repetitive code to places where it can be easily consolidated and managed. Throughout the book, you'll take a look at some of these features as they become useful to the project. Right now, it's time to look at custom form builders.

The Rails FormBuilder class is passed to a form_for block within a view (in the previous code, it's the class of the object named f). The FormBuilder class defines the instance methods used for the helpers such as text_field and password_field. Because FormBuilder is just an ordinary Ruby class, there's nothing stopping you from creating a custom subclass of FormBuilder that does whatever you want it to do with each helper method. All you need to do is tell the form_for method to use your builder instead of the default, which you do by passing a :builder => CustomClassName argument to the form_for call.

To build the custom form builder, you need a way to inject HTML into the output of the template from the builder code. You do this by using the @template instance variable, created by Rails, and referring to the code template currently being evaluated. The @template object has an instance method, content_tag, which takes the name of an HTML tag, a hash of the tag's attributes, and a block that resolves to the content inside the tag. The result of the content_tag call can then be placed in the template output.

Now, in planning out what this custom form builder is going to do, it's hard not to notice the pattern: calling text_field should result in a table row with a caption and the text field, and calling password_field should result in a table row with a caption and the password field. On the other hand, a select call should result in a table row with a caption and the select field. You could write each of those methods individually, but even if they all called a common general method, there's still a certain amount of repetition. Instead, I recommend writing all the similar methods at once using Ruby's metaprogramming capabilities, particularly the define_method method.

I'm going to present the first part of the code for the custom builder, and explain it. (A complete description of Ruby metaprogramming in general and define_method in particular is included in Chapter 14.) To begin, place the following class in models/tabular_form_builder.rb:

class TabularFormBuilder < ActionView::Helpers::FormBuilder

  def self.build_tabular_field(method_name)
    define_method(method_name) do | attribute, *args |
      options = args[0] || {}
      caption = options[:caption] || attribute.to_s.humanize
      caption_class = options[:caption_class] || "tdheader"
      options.delete(:caption)
      options.delete(:caption_class)
      @template.content_tag("tr") do
        @template.content_tag("td", :class => caption_class) do
          "#{caption}"
        end +
        @template.content_tag("td") do
          super(attribute, options)
        end
      end
    end
  end

  field_helpers.each do | method_name |
    build_tabular_field(method_name)
  end
end

Like a lot of Ruby metaprogramming code, this looks a little strange at first. The basic structure is to define a class method called build_tabular_field. This method is called with a single argument, and the effect of calling that method is to create an instance method whose name is that single argument. So calling build_tabular_field(:text_area) creates the instance method TabularFormBuilder .text_area.

The magic happens in the define_method call, which takes as an argument the name that will be used to call the new method and then a block. The arguments to the block will be the argument list for the new method, and the body of the block is the body of the new method. You may be familiar with metaprogramming by awkwardly building up a string that would be evaluated to create a dynamic program. Due to the nature of Ruby blocks, you don't have to do that — you can just write ordinary Ruby code in the block part of define_method, and the block automatically becomes the body of the newly created dynamic method. What does that block do inside define_method? First, it pulls an options hash out of the args list — given the way that the field helpers are called, the options hash will always be the second argument if it exists. However, you can't define a default value in a block argument list the way you can in a method argument list, so the *args notation is a somewhat awkward compromise between the normal call list of a field helper and the allowed semantics of a Ruby block.

From the options, the text and CSS class of the caption cell are retrieved. If they are not there, the text defaults to a humanized conversion of the attribute name, and the CSS class defaults to tdheader. The two new fields need to get removed from the options hash; otherwise, Rails will put them in the attribute lists of the table cells you're going to build, which would be a little annoying.

After that, a tr cell is created with a block that contains two td cells — notice the critically important plus sign between the two td cell blocks. Each content_tag call returns a string, and you have to be sure that the tr cell contents are the two td cells concatenated together. (If you leave off the plus sign, only the second cell will be included in the row, because it would be the last value of the block expression.) The first td cell contains only a string with the previously determined caption. The second cell actually calls super, which is the default FormBuilder rendering of a text field, password field, or whatever. The result is just placed in the second table cell.

The loop that comes after the build_tabular_field method is raw code — it's part of the class but not inside any method. That code is evaluated when the class is loaded, and it calls build_tabular_field once for each known field helper in the field_helper list. This should cleanly create all the methods needed for the new field helper to be a drop-in replacement for an existing field helper.

However, it turns out that submit is not in the field_helper list. Plus, it's probably useful to have a general method to put an arbitrary row into the field table. Add the following two methods to the TabularFormBuilder class:

def submit(caption, args={})
    row(super(caption, args))
  end

  def row(content)
    @template.content_tag("tr") do
      @template.content_tag("td", :colspan => 2) do
        "#{content}"
      end
    end
  end

This completes a simple form builder, but as you may have noticed, it creates all the tr and td tags but not the surrounding table. This can be done with some ordinary helper methods placed in application_helper.rb. Creating the helper method has the secondary advantage of encapsulating the form_for method with the builder argument, so you won't have to repeat that typing either. Add the following to application_helper.rb:

def convert_args(builder_class, args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    options = options.merge(:builder => builder_class)
    args = (args << options)
  end

  def table_form_for(name, *args, &proc)
    concat("<table>", proc.binding)
    form_for(name, *convert_args(TabularFormBuilder, args), &proc)
    concat("</table>", proc.binding)
  end

The table_form_for method should look straightforward — it uses concat (a standard way to inject code into the template from a handler method with a block) to surround the form_for call with table tags. The convert_args method, on the other hand, probably looks a little weird to you. The problem here is that form_for can be called with its options hash as the second argument, after the object being displayed in the form, or as the third argument, after the model class and the object. The convert_args method just makes sure that you add the builder argument to the actual hash, taking advantage of the fact that you know it's always the last argument.

One oddity about this code snippet is that it places the form tag inside the table tag. Technically, this is legal HTML, but it sure looks weird to my eyes.

With the helper in place, it's time for my favorite part — the actual new form under the custom form builder. The following code is an exact, drop-in replacement for the previous view code with all the tr and td tags:

<% table_form_for @user do |f| %>
    <%= f.text_field :first_name, :size => 15, :class => "input" %>
    <%= f.text_field :last_name, :size => 20, :class => "input" %>
    <%= f.text_field :username, :size => 15, :class => "input" %>
    <%= f.text_field :email, :class => "input" %>
    <%= f.text_field :email_confirmation, :class => "input" %>
    <%= f.password_field :password, :size => 10, :class => "input" %>
    <%= f.password_field :password_confirmation, :size => 10,
        :class => "input" %>
    <%= f.submit "Create" %>
<% end %>

Now that's more like it. With the extraneous table tags moved to the table builder, it's much easier to see exactly what the form defines. This code will be much easier to maintain going forward.

This form will get the user data into the system. The next step is to scramble the passwords before you save them.

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

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