Select list for inline editing with Ruby on Rails

I found it surprising that the Rails plugin for inline editing only supports a textfield as the control. Fortunately it’s pretty easy to roll your own. A Google search for “rails in-place select” turns up at least a few others who have had the same problem. I must first point out that I’m pretty new to Ruby and Rails, although I am very experienced with other languages: Java, C/C++, PHP, et.al.

I chose to implement mine by closely following the example of InPlaceMacrosHelper.in_place_editor().

Jed Prentice
#Development | Posted

I found it surprising that the Rails plugin for inline editing only supports a textfield as the control. Fortunately it’s pretty easy to roll your own. A Google search for “rails in-place select” turns up at least a few others who have had the same problem. I must first point out that I’m pretty new to Ruby and Rails, although I am very experienced with other languages: Java, C/C++, PHP, et.al.

I chose to implement mine by closely following the example of InPlaceMacrosHelper.in_place_editor(). I simply added a method to my application helper that is very similar to InPlaceMacrosHelper.in_place_editor(), but creates an instance of Prototype’s Ajax.InPlaceCollectionEditor and passes options specific to it in addition to the ones supported by InPlaceMacrosHelper.in_place_editor(). I figure if an in-place select shows up in that plugin down the road it will stick pretty close to this contract and I can easily replace mine.

For me, the key insight insight was that this task boils down to rendering Javascript strings in Ruby. This makes it a little trickier to debug when things aren’t working than if you are working with raw Javascript in a file or within a script tag in a page. Another trick was that, despite the supposed lack of strong types in Javascript, I still had to distinguish between numbers and strings for option values so Javascript would pass them correctly as request parameters. This is important if you are displaying persistent entities in the list, in which you display the name but need to pass the ID of the object.

Anyway, a picture is worth a thousand words, so here’s the full code listing:

  ##<br>  # Renders an in-place select similar to in_place_editor.  Options are the same as those supported by<br>  # InPlaceMacrosHelper.in_place_editor(), plus some extra ones to deal with the list:<br>  #<br>  # <tt>:collection</tt>::              The collection that will be used to build the list options<br>  # <tt>:load_collection_url</tt>::     A <span class="caps">URL</span> that will return the collection in <span class="caps">JSON</span> format<br>  # <tt>:loading_collection_text</tt>:: Text to display while the collection is loading<br>  # <tt>:loading_class_name</tt>::      Class applied to form while the collection is loading<br>  ##<br>  def in_place_select(field_id, options = {})<br>    function =  "new Ajax.InPlaceCollectionEditor("<br>    function << "'#{field_id}', "<br>    function << "'#{url_for(options[:url])}'"<br><br>    js_options = {}<br>    js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text]<br>    js_options['okText'] = %('#{options[:save_text]}') if options[:save_text]<br>    js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text]<br>    js_options['savingText'] = %('#{options[:saving_text]}') if options[:saving_text]<br>    js_options['rows'] = options[:rows] if options[:rows]<br>    js_options['cols'] = options[:cols] if options[:cols]<br>    js_options['size'] = options[:size] if options[:size]<br>    js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control]<br>    js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url]<br>    js_options['ajaxOptions'] = options[:options] if options[:options]<br>    js_options['evalScripts'] = options[:script] if options[:script]<br>    js_options['onComplete'] = options[:on_complete] if options[:on_complete]<br>    js_options['callback']   = "function(form) { return #{options[:with]} }" if options[:with]<br>    js_options['clickToEditText'] = %('#{options[:click_to_edit_text]}') if options[:click_to_edit_text]<br><br>    js_options['collection'] = %(#{js_collection_for(options[:collection])}) if options[:collection]<br>    js_options['loadCollectionURL'] = %('#{url_for(options[:load_collection_url])}') if options[:load_collection_url]<br>    js_options['loadingCollectionText'] = %('#{options[:loading_collection_text]}') if options[:loading_collection_text]<br>    js_options['loadingClassName'] = %('#{options[:loading_class_name]}') if options[:loading_class_name]<br><br>    function << (', ' + options_for_javascript(js_options)) unless js_options.empty?<br>    function << ')'<br>    javascript_tag(function)<br>  end<br><br>  private<br><br>    ##<br>    # Converts the given collection to a javascript string suitable for rendering options in a select list.<br>    # The collection key becomes the option value, while the collection value becomes the body of the <option> tags.<br>    ##<br>    def js_collection_for(collection)<br>      js = '['<br>      collection.each { |key, value| js << "[#{to_javascript(value)},#{to_javascript(key)}]," }<br>      js = js.chop<br>      js << ']'<br>    end<br><br>    ##<br>    # Surrounds the given value with single quotes if it's not a number so JavaScript will render/process the select<br>    # option values correctly.<br>    ##<br>    def to_javascript(value)<br>      return value if value.is_a?(Numeric)<br>      "'#{value}'"<br>    end

Jed Prentice