Monday, April 10

In-place select-and-submit for Ruby on Rails

If you'll recall from my last post (on my now deprecated blog), the built-in in-place-editting tools that come with Rails didn't provide exactly what I needed, so I had to roll my own. Well, it happened again; this time with a drop-down menu. I wanted to provide the user with a drop-down list that automatically updated the back-end, without refreshing the entire page, when an item was chosen from it.

Just like with my text in-place-editting solution, this one hides the drop-down element once an option is chosen so that the user can't quickly re-select another option causing a possible race condition. In its place, it shows the user a progress indicator, which for me is a little animated GIF of a spinning circular arrow and the phrase, "Saving..." Once the server has confirmed the value was saved, the element is restored back to a drop-down menu with the saved value pre-selected.

I ran into an odd little behavior while implementing this utility. I had originally tried to call "form.submit();" when the value of the selection was changed, but this resulted in the browser posting the form and completely ignoring the "onsubmit" directive declared in the form tag. This was frustrating, and Googling the issue didn't turn up anything useful. But, I discovered through trial and error that I could call "form.onsubmit();" directly and all was good.

So without furher ado, here's the method from my application_helper.rb file:

def select_and_submit( controller, action, id, element_for_results, options = {}, selected = nil, extra_hidden_values = {} )
label = "#{controller}_#{action}_#{id}"
extra_hidden_tags = ""
extra_hidden_values.each_pair do |key, value|
extra_hidden_tags << "<input id=\"#{key}\" name=\"#{key}\" type=\"hidden\" value=\"#{value}\" />"
end
options_rendered = ""
for value in options
if value.eql? selected then
selected_rendered = ' selected="selected"'
else
selected_rendered = ''
end
options_rendered << <<EOF
<option value="#{value.id}"#{selected_rendered}>#{value.name}</option>
EOF
end
<<EOF
<div id="#{label}_select">
<form id="#{label}_form" action="/#{controller}/#{action}" method="post" onsubmit="Element.hide( '#{label}_select' ); Element.show( '#{label}_saving' ); new Ajax.Updater('#{element_for_results}', '/#{controller}/#{action}', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
<input id="select_id" name="select_id" type="hidden" value="#{id}" />
#{extra_hidden_tags}
<select id="value_id" name="value_id" onchange="this.form.onsubmit(); return false;">
<option value=""></option>
#{options_rendered}
</select>
</form>
</div>
<div id="#{label}_saving">
<img src="/images/indicator_arrows_circle.gif" alt="*">
Saving...
</div>
<script>
Element.hide( '#{label}_saving' );
</script>
EOF
end

And here's how I call it:

<%= select_and_submit 'collectibles', 'set_value_for_attribute', attribute.id, "attribute_row_#{attribute.id}", attribute.values, collectible.get_value_for_attribute( attribute ), { :collectible_id => collectible.id } %>

2 comments:

Max said...

Interesting post! I had a problem with something similar. I've got a dropdown list where users selct something to add to a sortable list (lineup of bands playing a concert). The sortable list uses AJAX (and saves to the database after each update/movement).

I wanted the adding the element from the sortable list to be nice and AJAXified too. That wasn't easy. I've got it so that once the band is selected, it's add to the sortable list (in the DB), then the view is refreshed. If you don't refresh the view, the new element you've added doesn't sort (but all the others do).

Hell. I love Rails.

Paul said...

Great post. I was just pounding my head on the keyboard trying to figure it out. I wrote up my solution, and gave you credit due, on my blog at http://www.paulwelty.com/.

My need was to use this within a partial rendering a collection. Works just fine there, too.