4

In an effort to have everything translateable in our website ( including the error messages for the validations ), we switched almost all of our forms to remote forms. While this helps with the ability to translate error messages, we have encountered other problems, like:

  • if the user clicks on the submit button multiple times, the action gets called multiple times. If we have a remote form for creating a new record in the database, and assuming that the user's data is valid, each click will add a new object ( with the exact same contents ). Is there any way of making sure that such things cannot happen?

Is there somewhere I could read about remote forms best practices? How could I handle the multiple clicks problem? Is switching all the forms to remote forms a very big mistake?

Peter
  • 127,331
  • 53
  • 180
  • 211
Geo
  • 93,257
  • 117
  • 344
  • 520
  • You can put onclick disablers attributes to your commit buttons. When users click the button, it would be disabled, the request sent to the server and the user now can't click again in the button. – MurifoX Jun 14 '12 at 20:21
  • Yeah you can put a handler on the commit button to preventDefault on the event after one click. – Michael Papile Jun 14 '12 at 20:32
  • This isn't a rails problem. This should be dealt with using js. What you should do is wait for a response before you allow another request to be sent. That way you can return errors, and when the record is created successfully, replace the form with a success message. – Damien Roche Jun 14 '12 at 20:45
  • I'm aware it's not a Rails problem, but since Rails offers this functionality, it should also offer a way of handling such cases, right? – Geo Jun 14 '12 at 20:53

8 Answers8

3

There is a rails 3 option called :disable_with. Put this on input elements to disable and re-label them while a remote form is being submitted. It adds a data-disable-with tag to those inputs and rails.js can select and bind this functionality.

submit_tag "Complete sale", :disable_with => "Please wait..."

More info can be found here

Dty
  • 12,253
  • 6
  • 43
  • 61
  • I also tried this, but as soon as the response comes from the server, they are reenabled, and I'm faced with the possibility of the user clicking like crazy again :) – Geo Jun 15 '12 at 05:47
  • 2
    At this point I'm not exactly clear on what you're looking for. Usually with regular forms when the form is successful and a model is created the user is redirected to the model's `show` page. I guess you could do that. – Dty Jun 15 '12 at 06:33
2

Easy, and you can achieve that in many ways depending your preferences:

  1. Post the form manually simply using an ajax request and while you wait for the response disable/hide (or whatever you need) the form to ensure the user can't keep doing posts as crazy. Once you get the response from the server, again you can allow the user to post again (cleaning the form first), or show something else or redirect it to another page or again whatever you need.

  2. Use link_to :remote=>true to submit the form and add a callback function to handle the response and also to disable/hide (or whatever you need) the form when it's submitted

  3. Add a js listener to the form to detect when it's submitted and then disable/hide/whatever the form

As you see, there are lots of different ways to achieve what you need.

EDIT: If you need info about binding or handling a form submit from js here you'll find very easy and interesting examples that may help you to do what I suggested you! jQuery Submit

FedeX
  • 426
  • 2
  • 8
  • Will this involve moving away from the remote_form functionality? – Geo Aug 21 '12 at 11:06
  • If you want to keep using the remote_form functionallity, that's fine then you should just follow the third option I gave you. – FedeX Aug 21 '12 at 11:14
  • Hi Tempus, have a look at the link above. – FedeX Aug 21 '12 at 11:30
  • It does not feel like it's the best way for Rails. I mean, why offer developers the functionality for such things if there isn't an easy/reliable way of reducing/eliminating such scenarios? – Geo Aug 21 '12 at 13:41
  • I agree, none of the recommendations are rails specific or rely on rails/ruby technology. For remote forms, and your case imho, it is not needed to use jquery in this way. But since rails generates straight html/forms, you could most definitely resort to jquery and it will get the job done. And in some cases you will have to. – nathanvda Aug 23 '12 at 23:44
2

I have remote forms extensively myself, and in most cases I would avoid them. But sometimes your layout or UX demands for on-the-fly drop-down forms, without reloading or refreshing the complete page.

So, let me tackle this in steps.

1. Preventing Normal form double-post

Even with a normal form, a user could double-click your button, or click multiple times, if the user does not get a clear indication that the click has been registered and the action has started.

There are a lot of ways (e.g. javascript) to make this visible, but the easiest in rails is this:

= f.button  :submit, :disable_with => "Please wait..." 

This will disable the button after the first click, clearly indicating the click has been registered and the action has started.

2. Handling the remote form

For a remote form it is not that much different, but the difference most likely is: what happens afterward ?

With a remote form you have a few options:

  1. In case of error: you update the form with the errors.
  2. you leave the form open, allowing users to keep on entering the data (I think this is your case?)
  3. you redirect the users to some place.

Let me handle those cases. Please understand that those three cases are completely standard when doing a normal form. But not when doing a remote call.

2.1 In case of error

For a remote form to update correctly, you have to do a bit more magic. Not a lot, but a bit.

When using haml, you would have a view called edit.js.haml which would look something like

:plain
  $('#your-form-id').replaceWith('#{j render(:partial => '_form') }');

What this does: replace the complete haml with only the form. You will have to structure your views accordingly, to make this work. That is not hard, but just necessary.

2.2 Clearing the form

You have two options: * re-render the form completely, as with the errors. Only make sure you render the form from a new element, not the just posted one!! * just send the following javascript instead:

$('#your-form-id').reset();

This will blank the form, and normally, that would effectively render any following clicking useless (some client validation could block posting until some fields are filled in).

2.3 Redirecting

Since you are using a remote form, you can't just redirect. This has to happen client-side, so that is a tad more complicated.

Using haml again this would be something like

:plain
  document.location.href = '#{@redirect_uri}';

Conclusion

To prevent double (triple, quadruple, more) posts using remote forms you will have to

  • disable the button after first click (use :disable_with)
  • clear the form after succesful submission (reset the form or render with a new element)

Hope this helps.

nathanvda
  • 49,707
  • 13
  • 117
  • 139
1

I hate to say it, but it sounds like you've come up with a cure that's worse than the disease.

Why not use i18n for translations? That certainly would be the 'Rails way'...

If you must continue down this route, you are going to have to start using Javascript. Remote forms are usually for small 'AJAXy things' like votes or comments. Creating whole objects without leaving the page is useful for when people might want to create lots of them in a row (the exact problem you're trying to solve).

As soon as you start using AJAX, you have to deal with the fact that you'll have to get into doing some JS. It's client-side stuff and therefore not Rail's speciality.

If you feel that you've gone so far down this road that you can't turn back, I would suggest that the AJAX response should at least reset the form. This would then stop people creating the same thing more than once by mistake.

From a UI/UX point of view, it should also bring up a flash message letting users know that they successfully created the object.

So in summary - if you can afford the time, git reset and start using i18n, if you can't, make the ajax callback reset the form and set a flash message.

Edit: it just occurred to me that you could even get the AJAX to redirect the page for you (but you'd have to handle the flash messages yourself). However, using a remote form that then redirects via javascript is FUGLY...

Jules Copeland
  • 1,690
  • 17
  • 22
  • Initially, we started by using I18n for the translations, but then we got to the point where we had to maintain translation in 2 places, client side and server side. We also wanted to have the client side errors translateable. And we're on Heroku :) – Geo Aug 21 '12 at 17:24
  • 1
    why did you need it in both places? Also - have you ever checked out https://github.com/copycopter/copycopter-ruby-client ? – Jules Copeland Aug 21 '12 at 17:31
  • Ya... why would you have to duplicate the translations...? The 'client-side' is still *rendered* from the server, you should be able to use I18n just fine for both locations. I'm confused why you've obfuscated an already-solved problem. – nzifnab Aug 24 '12 at 17:36
1

I've had similar issues with using a popup on mouseover, and not wanting to queue several requests. To get more control, you might find it easier to use javascript/coffeescript directly instead of UJS (as I did).

The way I resolved it was assigning the Ajax call to a variable and checking if the variable was assigned. In my situation, I'd abort the ajax call, but you would probably want to return from the function and set the variable to null once the ajax call is completed successfully.

This coffeescript example is from my popup which uses a "GET", but in theory it should be the same for a "POST" or "PUT".

e.g.

jQuery ->
  ajaxCall = null

  $("#popupContent").html " "

  $("#popup").live "mouseover", ->
    if ajaxCall
      return

    ajaxCall = $.ajax(
      type: "GET"
      url: "/whatever_url"
      beforeSend: ->
        $("#popupContent").prepend "<p class=\"loading-text\">Loading..please wait...</p>"

      success: (data) ->
        $("#popupContent").empty().append(data)

      complete: ->
        $"(.loading-text").remove()
        ajaxCall = null
    )

I've left out my mouseout, and timer handling for brevity.

Catharz
  • 1,105
  • 10
  • 18
1

The simplest solution would be to generate a token for each form. Then your create action could make sure it hasn't been used yet and determine whether the record should be created.

Here's how I would go about writing this feature. Note that I haven't actually tested this, but the concept should work.

1. Inside the new action create a hash to identify the form request.

def new
  @product = Product.new
  @form_token = session["form_token"] = SecureRandom.hex(15)
end

2. Add a hidden field to the form that stores the form token. This will be captured in the create action to make sure the form hasn't been submitted before.

<%= hidden_field_tag :form_token, @form_token %>

3. In the create action you can make sure the form token matches between the session and params variables. This will give you a chance to see if this is the first or second submission.

def create
  # delete the form token if it matches
  if session[:form_token] == params[:form_token]
    session[:form_token] = nil
  else
    # if it doesn't match then check if a record was created recently
    product = Product.where('created_at > ?', 3.minutes.ago).where(title: params[:product][:title]).last

    # if the product exists then show it
    # or just return because it is a remote form
    redirect_to product and return if product.present?
  end

  # normal create action here ...
end

Update: What I have described above has a name, it is called a Synchronizer (or Déjà vu) Token. As described in this article, is a proper method to prevent a double submit.

This strategy addresses the problem of duplicate form submissions. A synchronizer token is set in a user's session and included with each form returned to the client. When that form is submitted, the synchronizer token in the form is compared to the synchronizer token in the session. The tokens should match the first time the form is submitted. If the tokens do not match, then the form submission may be disallowed and an error returned to the user. Token mismatch may occur when the user submits a form, then clicks the Back button in the browser and attempts to resubmit the same form.

On the other hand, if the two token values match, then we are confident that the flow of control is exactly as expected. At this point, the token value in the session is modified to a new value and the form submission is accepted.

Baylor Rae'
  • 3,995
  • 20
  • 39
  • This is actually a very interesting approach. But, I'd have to do this to all the remote forms on my website. Should they all have different names for the form_token, or should I stick to one? – Geo Aug 26 '12 at 08:16
  • @Tempus if you have more than one remote form per page, then yes you should probably use different names. But, if someone opened two different forms in separate tabs, which would overwrite the `form_token` session, it shouldn't cause too much of a problem because in the `else` statement, I included a query that checks the uniqueness of the record. – Baylor Rae' Aug 26 '12 at 12:57
  • All the solutions have good points to them , but this seems the most secure. Thanks everyone! – Geo Aug 27 '12 at 13:11
1

You can try something like that for ajax requests.

Set block variable true for ajax requests

  before_filter :xhr_blocker

  def xhr_blocker
    if request.xhr? 
      if session[:xhr_blocker]
        respond_to do |format|
          format.json, status: :unprocessable_entity
        end
      else
        session[:xhr_blocker] = true 
      end      
    end
  end

Clear xhr_blocker variable with an after filter method

  after_filter :clear_xhr_blocker

  def clear_xhr_blocker
    session[:xhr_blocker] = nil
  end
emrahbasman
  • 2,003
  • 10
  • 19
0

I would bind to ajax:complete, (or ajax:success and ajax:error) to redirect or update the DOM to remove/change the form as necessary when the request is complete.

aceofspades
  • 7,568
  • 1
  • 35
  • 48
  • Aren't `ajax:success` and `ajax:error` tied to whether or not the actual ajax call succeeded/failed, and not to updating/creating model instances? – Geo Aug 21 '12 at 22:03
  • Check out http://www.alfajango.com/blog/rails-3-remote-links-and-forms-data-type-with-jquery/. I would think you'll need to return a 200 but add some data with whatever status you'll need to know how to handle it in the browser. You could pass a redirect URL in the JSON for example, a message, or even javascript to execute. – aceofspades Aug 21 '12 at 23:09