Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
36
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

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