Search code examples
ruby-on-railsajaxremote-forms

How can we circumvent these remote forms drawback?


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?


Solution

  • 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.