Wednesday, June 16, 2010

Busy Button Benevolence

Rails as a lot of different ways of doing almost anything. Some of those are frowned on, others hailed as the 'right way'. Of course, the 'right way' changes depending on who you are speaking to, and to what the latest craze is in the rails community.

Buttons, specifically form submit buttons are no exception. My mission today was to prevent 'double clicks', which are a problem when the submit action takes long to resolve: users grow bored and stress test the button.

Ordinarily, one could simply rely on the rails goodness of :disable_with => 'some value'. However, this happens to break ajax forms. Soo...

After some searching on google and a couple of tries, I tossed all the crap out and went back to basics [people suggested using observers and the like to detect the click. Mission!].

Surely html tags have basic events, like onclick, onmouseover and the like? A quick test showed me that <input..../> indeed responds to onclick. So, all I had to do was wire the onclick to some js to turnoff the button.

Here is when I came up with:
<% form_remote_for @foo, :url => foo_path(@foo)  do |f| %>
<%= f.error_messages %>
<table cellspacing="0" cellpadding="0"
>
<caption>Update a foo <caption>
<%= render :partial => "form", :object => f %>
<tr>
<td colspan="2">
<%= submit_tag "Update", :onclick => "this.disabled=true;" %>

<
/td>
</tr>
</table>
<% end %>


Yup, that simple. It works partly because of how my page reloads/ajax is designed. The app uses a "content" div to flip ajax content. So, when a user hits 'Update' in this case, the div is replaced with a new 'show' partial or a new(same) 'edit' partial. Point is the button is rendered and you don't need to know to re-enable it on error.

Do do this, I use these little treasures of RJS goodness I concocted myself:

_create.rjs

if object.errors.empty?
#the two lines below allow us to call this magic for models associated with other controllers
path = "/#{object.class.name.underscore.pluralize}/"
class_name_underscore = object.class.name.underscore

#create a pass-through variable if none exists
eval "@#{class_name_underscore} = #{object.class}.find(#{object.id})" unless (eval "@#{class_name_underscore}")

page.insert_html :bottom, "list-body", :partial => "#{path}#{class_name_underscore}", :collection => [object]
page.replace_html "detail", :partial => "#{path}show"
page.visual_effect :highlight, "#{class_name_underscore}-#{object.id}", :endcolor=>"#c0c0c0", :restorecolor=>"#c0c0c0", :duration=>3
else
page.replace_html "detail", :partial => "#{path}new"
end
page.replace_html "flasher", :partial => "/flasher"


_update.rjs

if object.errors.empty?
#the two lines below alow us to call this magic for models associated with other controllers
path = "/#{object.class.name.underscore.pluralize}/"
class_name_underscore = object.class.name.underscore

#create a pass-through variable if none exists
eval "@#{class_name_underscore} = #{object.class}.find(#{object.id})" unless (eval "@#{class_name_underscore}")

page.replace "#{class_name_underscore}-#{object.id}", :partial => "#{path}#{class_name_underscore}", :collection => [object]
page.replace_html "detail", :partial => "#{path}show"
page.visual_effect :highlight, "#{class_name_underscore}-#{object.id}", :endcolor => "#c0c0c0", :restorecolor => "#c0c0c0", :duration=>3
else
page.replace_html "detail", :partial => "#{path}edit"
end
page.replace_html "flasher", :partial => "/flasher"
flash.discard


You keep these in app/views/ and forget about them. They do all the heavy lifting ajax in my projects, now that's DRY.

1 comment:

  1. An update to this problem:

    The technique decissed above does work aon all decent browsers. However, it breaks IE.

    It seems IE processes all the instructions first, and the does the ajax post. If one of the instructions is "disable", the button is rendered inactive and IE decides not to post the ajax after all.

    One way around it would be to use a scheduled function (possibly through an observer), but honestly it's not worth the effort IMO.

    I've opter to rather reply on the "processing..." animation which lets the user know we are busy and they should lay off the buttons.

    ReplyDelete