uberllama
2009-Jul-16 23:08 UTC
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 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. Any and all help would be massively appreciated. I will even send you a facebook gift. ;)
David A. Black
2009-Jul-17 00:48 UTC
Re: Bizarre issue with form builders (merging options and *args).
Hi -- On Thu, 16 Jul 2009, uberllama wrote:> 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? :-) and you should be OK (or very close to it). David -- David A. Black / Ruby Power and Light, LLC Ruby/Rails consulting & training: http://www.rubypal.com Now available: The Well-Grounded Rubyist (http://manning.com/black2) Training! Intro to Ruby, with Black & Kastner, September 14-17 (More info: http://rubyurl.com/vmzN)
Marnen Laibow-Koser
2009-Jul-17 14:25 UTC
Re: Bizarre issue with form builders (merging options and *args).
uberllama wrote: [...]> 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).[...] This is really not a good way of doing it. HTML and JS work best together when they are in completely separate files -- so don''t put your onclick and onchange handlers in the form builder. (I know Rails encourages embedding JS in HTML, but that''s a problem with Rails.) Rather, attach the handlers in the (separate) JS file: $(''whatever'').onClick = function () { ... }, or some such (Prototype''s event handler enhancements may be fun here). Best, -- Marnen Laibow-Koser http://www.marnen.org marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.