[Rails] in_place_collection_editor

Ruairi Mc Comb ruairi at thecomb.net
Sat Mar 25 17:31:13 GMT 2006


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+.


More information about the Rails mailing list