Hi, I''m trying to write a helper for Scriptaculous'' InPlaceCollectionEditor component. I''ve already submitted a patch (http://dev.rubyonrails.org/ticket/4302). This was a drunk patch; it needs a bit of work (Don''t drink & code!). So far I''ve gotten it to work correctly with normal collections, but I want to use it for belongs_to relations as well. I want to build a test case for the in_place_collection_editor field for which I need to know how to do the following (in rails core trunk): How do I simulate a controller? How do I simulate an instance variable on that controller? How do I simulate an Array of ActiveRecord instances? Can this array be loaded from fixtures? I basically want to do the following: The model: class Post < ActiveRecord::Base belongs_to :section end class Section < ActiveRecord::Base has_many :posts end There would be a "post" instance variable set on the controller. I''ve tried setting this up as I''ve seen in other tests (using structs) as such: def setup @sections = [ Section.new(:id => 1, :name => ''News''), Section.new(:id => 2, :name => ''Gossip''), Section.new(:id => 3, :name => ''Slander'') ] @post = Post.new(:id => ''1'', :title => ''Foo'', :section => @sections[0]) @controller = Class.new do def url_for(options, *parameters_for_method_reference) url = "http://www.example.com/" url << options[:action].to_s if options and options[:action] url end end @controller = @controller.new end But I''m not able to use this setup as ActionView::Helpers::InstanceTag uses a method of CGI to get to the instance variable set on the controller: def object @object || @template_object.instance_variable_get("@#{@object_name}") end Also the structs don''t seem to behave as I expected them to (they don''t seem to respond to .send, nor am I able to access properties), weird thing is that ActiveRecord objects do behave. I''d like to be able to develop this using tests. When I''m developing this as a plugin it''s a PITA to have to restart the server for the changes to reload. Anybody interested in the code (in plugin form) can check it out from: http://ruairimccomb.com/svn/in_place_collection_editor/ -------------- next part -------------- Index: actionpack/test/template/java_script_macros_helper_test.rb ==================================================================--- actionpack/test/template/java_script_macros_helper_test.rb (revision 3940) +++ actionpack/test/template/java_script_macros_helper_test.rb (working copy) @@ -9,8 +9,20 @@ include ActionView::Helpers::TextHelper include ActionView::Helpers::FormHelper include ActionView::Helpers::CaptureHelper - + + silence_warnings do + Section = Struct.new(:id, :name) + Post = Struct.new(:id, :title, :section) + end + def setup + @sections = [ + Section.new(:id => 1, :name => ''News''), + Section.new(:id => 2, :name => ''Gossip''), + Section.new(:id => 3, :name => ''Slander'') + ] + @post = Post.new(:id => ''1'', :title => ''Foo'', :section => @sections[0]) + @controller = Class.new do def url_for(options, *parameters_for_method_reference) url = "http://www.example.com/" @@ -91,4 +103,30 @@ :load_text_url => { :action => "action_to_get_value" }) end + def test_in_place_collection_editor_with_simple_array + assert_match "Ajax.InPlaceCollectionEditor(''id-goes-here'', ''http://www.example.com/action_to_set_value'', {collection:[''1'',''2'',''3'']})", + in_place_collection_editor(''id-goes-here'', + :url => { :action => "action_to_set_value" }, + :collection => [1,2,3] + ) + end + + def test_in_place_collection_editor_with_multi_array + assert_match "Ajax.InPlaceCollectionEditor(''id-goes-here'', ''http://www.example.com/action_to_set_value'', {collection:[[''1'',''one''],[''2'',''two''],[''3'',''three'']]})", + in_place_collection_editor(''id-goes-here'', + :url => { :action => "action_to_set_value" }, + :collection => [[1,''one''],[2,''two''],[3,''three'']] + ) + end + + def test_in_place_collection_editor_field + assert_match "<span class=\"in_place_editor_field\" id=\"post_section_in_place_editor\"></span><script type=\"text/javascript\">\n//<![CDATA[\nnew Ajax.InPlaceCollectionEditor(''post_section_in_place_editor'', ''http://www.example.com/'', {collection:[''1'',''2'',''3'']})\n//]]>\n</script>", + in_place_collection_editor_field(''post'', ''section'', @sections) + end + + def test_in_place_association_editor_field + assert_match "<span class=\"in_place_editor_field\" id=\"post_section_in_place_editor\"></span><script type=\"text/javascript\">\n//<![CDATA[\nnew Ajax.InPlaceCollectionEditor(''post_section_in_place_editor'', ''http://www.example.com/'', {collection:[[''1'',''News''],[''2'',''Gossip''],[''3'',''Slander'']]})\n//]]>\n</script>", + in_place_association_editor_field(''post'', ''section'', @sections, ''id'', ''name'') + end + end Index: actionpack/lib/action_view/helpers/java_script_macros_helper.rb ==================================================================--- actionpack/lib/action_view/helpers/java_script_macros_helper.rb (revision 3940) +++ actionpack/lib/action_view/helpers/java_script_macros_helper.rb (working copy) @@ -72,6 +72,86 @@ tag.to_content_tag(tag_options.delete(:tag), tag_options) + in_place_editor(tag_options[:id], in_place_editor_options) end + + # Makes an HTML element specified by the DOM ID +field_id+ become an in-place + # editor of a property using a selection list populated by the . + # + # A form is automatically created and displayed when the user clicks the element, + # something like this: + # <form id="myElement-in-place-edit-form" target="specified url"> + # <select> + # <option value="1">One</option> + # </select> + # <input type="submit" value="ok"/> + # <a onclick="javascript to cancel the editing">cancel</a> + # </form> + # + # The form is serialized and sent to the server using an AJAX call, the action on + # the server should process the value and return the updated value in the body of + # the reponse. The element will automatically be updated with the changed value + # (as returned from the server). + # + # Required +options+ are: + # <tt>:url</tt>:: Specifies the url where the updated value should + # be sent after the user presses "ok". + # <tt>:collection</tt>: The collection to build the select from. This can be either + # simple array, or a multilevel array. + # + # + # Addtional +options+ are: + # <tt>:cancel_text</tt>:: The text on the cancel link. (default: "cancel") + # <tt>:save_text</tt>:: The text on the save link. (default: "ok") + # <tt>:loading_text</tt>:: The text to display when submitting to the server (default: "Saving...") + # <tt>:external_control</tt>:: The id of an external control used to enter edit mode. + # <tt>:load_text_url</tt>:: URL where initial value of editor (content) is retrieved. + # <tt>:options</tt>:: Pass through options to the AJAX call (see prototype''s Ajax.Updater) + # <tt>:with</tt>:: JavaScript snippet that should return what is to be sent + # in the AJAX call, +form+ is an implicit parameter + def in_place_collection_editor(field_id, options = {}) + function = "new Ajax.InPlaceCollectionEditor(" + function << "''#{field_id}'', " + function << "''#{url_for(options[:url])}''" + + js_options = {} + js_options[''collection''] = "[#{options[:collection].collect{|i| array_or_string_for_javascript(i)}.join('','')}]" if options[:collection] + js_options[''value''] = %(''#{options[:value]}'') if options[:value] + js_options[''cancelText''] = %(''#{options[:cancel_text]}'') if options[:cancel_text] + js_options[''okText''] = %(''#{options[:save_text]}'') if options[:save_text] + js_options[''loadingText''] = %(''#{options[:loading_text]}'') if options[:loading_text] + js_options[''externalControl''] = "''#{options[:external_control]}''" if options[:external_control] + js_options[''loadTextURL''] = "''#{url_for(options[:load_text_url])}''" if options[:load_text_url] + js_options[''ajaxOptions''] = options[:options] if options[:options] + js_options[''callback''] = "function(form) { return #{options[:with]} }" if options[:with] + function << ('', '' + options_for_javascript(js_options)) unless js_options.empty? + + function << '')'' + + javascript_tag(function) + end + + # Renders the value of the specified object and method with in-place editing capabilities. + # + def in_place_collection_editor_field(object, method, collection, tag_options = {}, in_place_collection_editor_options = {}) + tag = ::ActionView::Helpers::InstanceTag.new(object, method, self) + tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options) + in_place_collection_editor_options[:collection] = collection # .collect {|c| [c[value_method],c[text_method]]} + in_place_collection_editor_options[:value] = tag.object.send(method) + in_place_collection_editor_options[:url] = in_place_collection_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}", :id => tag.object.id }) + tag.to_content_tag(tag_options.delete(:tag), tag_options) + + in_place_collection_editor(tag_options[:id], in_place_collection_editor_options) + end + + # Renders the value of the specified objects association with in-place editing capabilities. + # + def in_place_association_editor_field(object, method, collection, value_method, text_method, tag_options = {}, in_place_collection_editor_options = {}) + tag = ::ActionView::Helpers::InstanceTag.new(object, method, self) + tag_options = {:tag => "span", :id => "#{object}_#{method}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options) + in_place_collection_editor_options[:collection] = collection.collect{|e| [e[value_method],e[text_method]]} + in_place_collection_editor_options[:value] = tag.object.send(method).send(text_method) + in_place_collection_editor_options[:url] = in_place_collection_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}" }) + content_tag(tag_options.delete(:tag), tag.object.send(method).send(text_method), tag_options) + + in_place_collection_editor(tag_options[:id], in_place_collection_editor_options) + end # Adds AJAX autocomplete functionality to the text input field with the # DOM ID specified by +field_id+.