Help with custom FormBuilder

Hi all

I’m working on a custom FormBuilder that should be able to handle habtm
and has_many relationships with just one single method call, similar to
the existing text_field, text_area etc. methods.

My code in a view looks like that:

<%= error_messages_for ‘member’ %>

<%= form.text_field :email %>
<%= form.text_area :signature %>
<%= form.has_and_belongs_to_many :preferred_music_styles %>
<%= form.belongs_to :origin_country %>

And this results in a well-known textfield and textarea for email and
signature AND in checkboxes for the habtm relation AND a dropdown select
list for the belongs_to association.

Anyway, I have adapted some code from the Rails Recipes book and it
looks like the following so far:

def self.create_tagged_field(method_name)
  define_method(method_name) do |label, *args|
    html_surround(label, super) # Provides the HTML code that

surrounds the form label and the method (super) that generates the input
tag(s)
end
end

field_helpers.reject{|h| h == 'hidden_field'}.concat([:select]).each

do |name|
create_tagged_field(name)
end

def html_surround(label, content, options = {})
  label_options = {}
  label_options[:for] = "#{@object_name}_#{label}" unless

options[:print_label_for]
@template.content_tag(:div,
@template.content_tag(:div,
@template.content_tag(:label,
nil || label.to_s.humanize, # TODO: optional argument
:caption instead of nil!
label_options
), :class => :label
) + @template.content_tag(‘div’,
content, :class => :field), :class => :row
)
end

Now I’d like to offer some more options to the programmer, e.g. to set a
custom caption instead of using the name of the database field:

<%= form.belongs_to :origin_country, :caption => ‘Where do you come
from?’ %>

Sadly I have no clue where to catch this option and remove it from the
options for further processing, because so far Rails creates a “caption”
attribute in the input tag with the given value:

This is for sure not the wished effect, and it pollutes my perfectly
valid HTML code! :slight_smile:

So can anybody tell me how to

  1. display this custom label instead of the default caption when this
    option is set and
  2. how to prevent this option from being transmitted to the later Rails
    processings?

Thanks a lot for help.
Josh

Josh,

I’ve spent a bit of time with custom form builders. I originally
started with help from the cookbook, but later branched out on my own,
almost rewriting many of the field helpers to fit exactly what I
wanted the output to be.

Now, I don’t see where in your code you’re handling your own
belongs_to helper, but I imagine it just does some processing to get
the list of options and calls the select helper. The idea is to
manipulate the options rather than simply passing them on as-is.
Inspect args from the end (args.last) and see if it’s a hash. If so,
those are your options (and if not, no options were supplied). The
important thing to remember is that you want to manipulate the args,
passing some of them on to html_surround and keeping them from the
form helper (e.g. select). This means you have to provide explicit
arguments to super. (Otherwise, the parent class’s method will be
called with the original arguments.)

So take

html_surround(label, super)

and change it to

manipulation goes here

html_surround(label, super(arguments_for_super),
options_for_html_surround)

The select helper is a little trickier than the rest because it has
two sets of options (‘options’ and ‘html_options’) where the rest only
have one.


-yossef

Thanks for these hints, I’m gonna check it out. :slight_smile:

I’ve got another question…

def self.create_tagged_field(method_name, options = {})
  define_method(method_name) do |label, *args|
    html_surround(label, super, options) # Provides the HTML code 

that surrounds the form label and the method (super) that generates the
input tag(s)
end
end

field_helpers.reject{|h| h == 

‘hidden_field’}.concat([‘has_and_belongs_to_many’, ‘belongs_to’]).each
do |name|
label_clickable = true unless name == ‘has_and_belongs_to_many’
create_tagged_field(name, :label_clickable => label_clickable)
end

How do I find out in define_method part what id attribute the created
form field method will get?

So far I’m guessing it myself in html_surround:

caption_options[:for] = “#{@object_name}_#{label}”

But sometimes this is not correct. Using a text_field it generates

member_nickname

which is fine but in other cases - e.g. a collection_select - it
generates

member_origin_country

which is often incorrect because the ID depends there on the association
name itself. In my case it should be member_country_id.

So I guess there’s a way to examine the ID that Rails will assign to the
form element — but how? :slight_smile:

Thanks
Josh

Josh,

About knowing the ID of the form element, there are methods in Action
Pack for that. I can’t recall them off the top of my head, and to be
honest, some of my immense form builder bypassed and provided its own
IDs, so I knew exactly what was going on. But look for something like
“assign_default_name_and_id” and you’ll see what’s going on.

As for your Array#merge problem, if I’m not mistaken it’s just that
you’re calling super(label, args) instead of super(label, *args). See
this:

def args_length(*args)
args.length
end

def blah(arg1, *args)
args_length(args)
end

blah(‘test’)
=> 1
blah(‘test’, 1, 2, 3, 4)
=> 1

def blah(arg1, *args)
args_length(*args)
end

blah(‘test’)
=> 0
blah(‘test’, 1, 2, 3, 4)
=> 4

And there’s one thing that jumps out at me. You’ve changed it so
you’re calling super for your brand-new association methods
(has_and_belongs_to_many, belongs_to), but the parent class doesn’t
have them (I assume).

-yossef

Hrm about your above answer… I changed it to the following:

def self.create_tagged_field(method_name, options = {})
  define_method(method_name) do |label, *args|
    html_surround(label, super(label, args), options) # Provides the 

HTML code that surrounds the form label and the method (super) that
generates the input tag(s)
end
end

But now I get the following error:

undefined method `merge’ for []:Array