Wednesday, April 18, 2007

Rails & JavaScript tip: Disable submit button on all forms after submit

(UPDATE: SEE COMMENTS FOR CODE UPDATE)

I'm still testing this, but so far it works. Please let me know if you come across any bugs with it.

If you've ever watched non-tech savy people use a computer you've seen them double-click EVERYTHING. Including submit buttons on forms. Which leads to double-data entry.

Eew.

I didn't find anything built into Rails to deal with this (I could have missed it). I've coded a JavaScript solution that applies itself to every form in my web application, Pudding, without having to do anything to the forms. The exception to this are the Ajax forms. I'm not happy with my solution, but I've got it working. (If anyone sees a better way to do this, please let me know!)

For the code below to work you need to be using Ruby on Rails, the "submit_tag" function to generate all your submit buttons, and have the Prototype JavaScript library included in your pages. (You can adapt this to your own framework, but you've got to have a consistent name for your submit buttons. "submit_tag" gives every submit button the name, "commit". And I'm using Prototype's functions to make things easier for me.)


var FormHelper = {
disableSubmitButtonByEvent: function(event) {
var theForm = Event.element(event);
var submitButton = theForm.commit;
this.disable(submitButton);
},

disableSubmitButton: function(theForm) {
var submitButton = theForm.commit;
this.disable(submitButton);
},

disable: function(submitButton) {
submitButton.value = "Processing...";
$(submitButton).disable();
}
};


function addFormHelperListener() {
var formsInPage = document.forms;
for (var i=0; i<=formsInPage.length; i++) {
Event.observe(formsInPage[i],
'submit',
FormHelper.disableSubmitButtonByEvent.bindAsEventListener(FormHelper));
}
}

function addListeners(e) {
addFormHelperListener();
}

Event.observe(window, 'load', addListeners);



How does this all work?

Starting from the bottom of the code...

The Event.observe(window, 'load', addListeners); line forces your web browser to call the addListeners(e) function once the page is done loading. The addListeners(e) function is a great place to put all function calls that "setup" event listeners.

The addFormHelperListener() function is then called. It loops over all the forms on your page and ties the submission of the form to the disableSubmitButtonByEvent function up there in that FormHelper object. The disableSubmitButtonByEvent takes a look at the event that fired off (the submission of the form) and pulls out the DOM element that caused it - the form.

Since the Ruby on Rails function, "submit_tag", is used on all my forms they contain the HTML attribute, name="commit". Which allows me to grab the submit button out of the form object. Then it's easy! You just change the value of the submit button and then disable it.

Now the user can't double click the submit button!

As I mentioned earlier, the code works for all the forms on my web application except the Ajax ones. See that function, disableSubmitButton?

When you're using the "form_remote_tag" function you just add this option and you're good to go.



:before => "FormHelper.disableSubmitButton(this)"



A full example looks like this:



<% form_remote_tag(:update => [ID OF ELEMENT TO UPDATE],
:before => "FormHelper.disableSubmitButton(this)",
:url => {:controller => 'piece',
:action => 'new_comment'},
:complete => "[I'M CALLING SOME JAVASCRIPT TO SYNC UP SOME PAGE ELEMENTS.]") do -%>



I hate tacking on that ":before" option, but I couldn't figure out why it wasn't working. I'm guessing it's got something to do with the ":complete" option....

FUN FACT:
I had originally written this:



for (var i=0; i<=formsInPage.length; i++)



like this:



for (var currentForm in formsInPage)



But it didn't work in Safari.

Safari, you're on notice.

Labels: ,

8 Comments:

Anonymous Tyler Hunt said...

Wouldn't this be a lot simpler?

$$('form').each(function(form) {
form.observe('submit', function() {
form.getInputs('submit').each(function(submit) {
submit.disable();
});
});
});

Thursday, April 19, 2007 8:58:00 AM  
Blogger Michael Sica said...

Hi Tyler,

Thanks for the snippet!

I just finished putting it into my app, and it works! I definitely like how it's not bound to having the name="commit" attribute in the submit button.

Additionally, I figured out what why my Ajax form was acting up. My guess is that since the page doesn't load again after the submission of the Ajax form the JavaScript engine believes that the submit event has already fired (which it has) and will not react to a second submit event. The solution is to call the code that set's up the listener after the form's Ajax work is completed.

My code is now this:

--------
JavaScript
--------

function addFormListener() {
$$('form').each(function(form) {

form.observe('submit', function() {
form.getInputs('submit').each(function(submit) {
submit.value = "Processing...";
submit.disable();
});
});
});
}

function addListeners(e) {
addPieceNameFillerListener();
addFormListener();
}

Event.observe(window, 'load', addListeners);

------
RHTML
------
<% form_remote_tag(:update => html_comment_container_id(piece),
:url => {:controller => 'piece',
:action => 'new_comment'},
:complete => "addFormListener();") do -%>

Sunday, April 22, 2007 5:35:00 PM  
Blogger Michael Sica said...

Just a point of clairification.

The RHTML code (above) is missing a function call. In the "complete" option I've got a call that reset's the form's values, including the submit button values.

If I posted all the code it would take forever to explain because it's part of a (somewhat) complicated series of events that get the form in sync with what's on the page.

Just know that you've got to reset the values of your Ajax form, and make sure you're resetting the submit button so your user can click on it.

Wednesday, April 25, 2007 12:27:00 AM  
Blogger Roger said...

The submit_tag method accepts an option called disable_with which will be used to disable the button when it is clicked. See the api.

Not sure if there is any thing like this for your Ajax forms though.

Thursday, May 10, 2007 11:19:00 AM  
Blogger Michael Sica said...

NO!!! It can't be that easy Roger! :)

I'll try it out. Boy I feel retarded!

Thursday, May 10, 2007 8:22:00 PM  
Anonymous Adam said...

(sorry for the late comment...)

The disable_with option works great except it breaks AJAX (form_remote) forms by bypassing the AJAX functionality and submitting the form using traditional methods. So, here's a workaround.

If you've loaded the prototype javascript library (which you kinda need if you're using form_remote), then you can use its built-in methods to disable the submit button.

form_remote_for :item,
:update=>'something',
:url=>{:action=>'action'},
:loading=>"Form.Element.disable('commit')",
:complete=>"Form.Element.enable('commit')"

Of course, this does depend on adding :id=>"commit" to the submit tag.

Anyway, just another way to do it.

Sunday, October 14, 2007 12:56:00 AM  
Anonymous Anonymous said...

Hallo.

I must confess I don't really like disabling Submit from javascript. I've been on too many flakey network links where it dies *right* when I submit, and then I need to reload the page and re-enter all the data ... it's a big pain, and unless I *really* want what the page has, I'll just give up.

My solution: just slap validates_uniqueness_of on the record it's creating. That way, even if they click Publish 17 times before it manages to display the new page, it still just adds it once, with maybe a little box that says "You already said that!". For example, a Comment class might have validates_uniqueness_of :content, :scope => [:user_id, :page_id].

But, anyway, I found this page while looking for how to do some simple javascript from Rails, which this page helped me figure out, so thanks!

Tuesday, November 20, 2007 7:29:00 PM  
Anonymous Anonymous said...

Thanks! Useful blog post, and great comments.

@Anonymous - If you need to post a comment or send mail with the submit button then you need this functionality.

Sunday, March 16, 2008 10:04:00 PM  

Post a Comment

<< Home