Bizarre issue with form builders (merging options and *args).

Okay guys, I've been beating my head against the wall with this one all day and was hoping I might garner some enlightenment from the gathered geniuses. I am including some pared down code to demonstrate my issue.

Basically, I want my form builder to automatically add an :onchange handler to a specific form field (for my example, I am using

My problem is that unless I pass in at least one hash value to the field helper, the option doesn't get added. i.e.:

[b]These work (the onclick will get added):[/b]

[code]<%= f.text_field :title, :class => 'sexy' %>[/code] [code]<%= f.text_field :title, :label => 'Topic' %>[/code] [code]<%= f.text_field :title, :class => 'sexy', :label => 'Topic' %>[/ code]

[b]This does not (no onclick, very sad):[/b] [code]<%= f.text_field :title %>[/code]

I have appended question marks to the blocks in question below.

[code] class ExampleFormBuilder < ActionView::Helpers::FormBuilder

  def self.create_tagged_field(method_name)     define_method(method_name) do |attr, *args|       options = args.extract_options!

      # ?? This doesn't work unless the field helper in the view is passing in a hash of options. ??       #options.merge!(:onclick => "alert('foo');")       # ?? Direct assignment doesn't work either ??       #options[:onclick] = "alert('foo');"

      label_text = "#{options[:label] || attr.to_s.humanize}:"       label = @template.content_tag('label', label_text, :for => "# {@object_name}_#{attr}")

      # remove my custom hash key so it doesn't pollute the output       options.delete_if {|key, value| key == :label}

      # ?? I don't need to do this, but why ??       #args = (args << options) unless options.blank?

      @template.content_tag('p', label + '<br />' + super)     end   end

  field_helpers.each do |name|     create_tagged_field(name)   end

end [/code]

Even if I am not passing in a hash, the extract_options! command should create a hash object from the args, so I don't know what I'm missing here.

My second part to this question is around the splat operator, *args, and options. Let's say I pass the field helper the option :class => 'sexy'. Now, since I am extracting the options hash from the args, I assumed I needed to add them back in before calling super. But spookily enough, I don't. Do the options extracted from *args maintain a reference, or am I missing something completely obvious.

Any and all help would be massively appreciated. I will even send you a facebook gift. :wink:

Hi --

Okay guys, I've been beating my head against the wall with this one all day and was hoping I might garner some enlightenment from the gathered geniuses. I am including some pared down code to demonstrate my issue.

Basically, I want my form builder to automatically add an :onchange handler to a specific form field (for my example, I am using an :onclick with a generic form builder).

My problem is that unless I pass in at least one hash value to the field helper, the option doesn't get added. i.e.:

[b]These work (the onclick will get added):[/b]

[code]<%= f.text_field :title, :class => 'sexy' %>[/code] [code]<%= f.text_field :title, :label => 'Topic' %>[/code] [code]<%= f.text_field :title, :class => 'sexy', :label => 'Topic' %>[/ code]

[b]This does not (no onclick, very sad):[/b] [code]<%= f.text_field :title %>[/code]

I have appended question marks to the blocks in question below.

[code] class ExampleFormBuilder < ActionView::Helpers::FormBuilder

def self.create_tagged_field(method_name)    define_method(method_name) do |attr, *args|      options = args.extract_options!

     # ?? This doesn't work unless the field helper in the view is passing in a hash of options. ??      #options.merge!(:onclick => "alert('foo');")      # ?? Direct assignment doesn't work either ??      #options[:onclick] = "alert('foo');"

     label_text = "#{options[:label] || attr.to_s.humanize}:"      label = @template.content_tag('label', label_text, :for => "# {@object_name}_#{attr}")

     # remove my custom hash key so it doesn't pollute the output      options.delete_if {|key, value| key == :label}

     # ?? I don't need to do this, but why ??      #args = (args << options) unless options.blank?

     @template.content_tag('p', label + '<br />' + super)    end end

field_helpers.each do |name|    create_tagged_field(name) end

end [/code]

Even if I am not passing in a hash, the extract_options! command should create a hash object from the args, so I don't know what I'm missing here.

My second part to this question is around the splat operator, *args, and options. Let's say I pass the field helper the option :class => 'sexy'. Now, since I am extracting the options hash from the args, I assumed I needed to add them back in before calling super. But spookily enough, I don't. Do the options extracted from *args maintain a reference, or am I missing something completely obvious.

Let me start with the last question first.

Short answer: super with implicit arguments, when you use define_method and a block, doesn't work the same as it does when you user super in a def-based method definition. You have to provide the arguments explicitly.

Longer answer:

If you do this:

   class A      def m(*args)        print "In A#m: "        p args      end    end

   class B < A      def m(*args)        print "In B#m: "        p args        args = ["hi!"]        super      end    end

   B.new.m(1,2,3)

you get this:

   In B#m: [1, 2, 3]    In A#m: ["hi!"]

The call to super uses the variable args -- not even the object that was bound to args originally, but the variable args -- to make the call to A#m. Now, look at this variation. Assume the same A, and then:

   class C < A      define_method(:m) do |*args|        print "In C#m: "        p args        args = ["hi!"]        super      end    end

   C.new.m(1,2,3)

This gives you this output:

   In C#m: [1, 2, 3]    In A#m: [1, 2, 3]

This time, the variable name "args", which comes from a block parameter (and not a method parameter), doesn't play the same role. Instead, as far as I can tell, super is using a copy of the original object that was bound to args. Even adding elements to args doesn't cause A#m to produce anything different.

And... you will be very interested in what happens if I run the above under Ruby 1.9.1:

In C#m: [1, 2, 3] sup.rb:22:in `block in <class:C>': implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly. (RuntimeError)    from sup.rb:27:in `<main>'

In other words, you can't do it any more anyway -- probably because it didn't really work in the first place, so it's gone.

So... change super to super(attr, *args), restore the args << options thing (but write it more simply, like this:

   args << options unless options.empty?

:slight_smile: and you should be OK (or very close to it).

David