Search code examples
javascriptruby-on-railsajaxturbolinksruby-on-rails-4.2

Rails 4.2, AJAX form is submitted exponentially to the server on each subsequent triggering


I have a select on a form (company select), changing the option selected will update the possible options in another select (address select). The options of the address select are updated by submitting an AJAX request. The options of the address select are successfully updated each time.

However, when the first company is selected the AJAX get request is sent 1 time. When a second company is selected, the AJAX request is sent 2 times. Then 4, then 8, then 16, then 32 etc.

Not ideal...

Final note, i've been trying to follow Brandon Hilkert's advice on organizing javascript.

app/views/layouts/application.html.erb

<!DOCTYPE html>
  <html lang="en">
    <head>
    <%# Disables turbolinks caching of pages, to allow smooth animations %>
    <meta name="turbolinks-cache-control" content="no-preview">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
    <%= favicon_link_tag 'logos_and_banners/ecl_logo.png' %>
    <title>Environmental Concern Ltd - Your future in our hands.</title>

    <%# Geocompete JQuery plugin  %>
    <script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?key=AIzaSyDfo9YlYY6BnemhdpDMlQbo6li3RyRYO-0&libraries=places"></script>
    <%= render partial: 'shared/flash' %>
    <%= render 'shared/navigation/navigation' %>
    <%# console %>
  </html>

app/views/licenses/_form.html.erb

<%= simple_form_for([@company, @license], remote: true, html: {role: 'form'}) do |f| %>
  <%# errors will be loaded here via AJAX  %>
  <div id="form-errors" class="alert alert-danger collapse"></div>

  <form role="form">
    <div class="form-inputs">
      <%= f.association :company,
            collection: Company.sorted_by('company_name_asc'),
            label_method: :company_name,
            value_method: :id,
            include_blank: false %>
      <%= f.association :address,
            collection: @company.addresses.sorted_by('site_name_asc'),
            label_method: :site_name,
            value_method: :id,
            include_blank: true %>
      <span class="help-block">
        Certain License types are related to a specific Address, others are only linked to the Company.
      </span>
      <%= f.input :license_type,
            collection: License::ALL_VALID_LICENSE_TYPES %>
      <%= f.input :license_name, as: :hidden%>
      <%= f.input :license_no %>
      <%= f.input :expiry_date,
            as: :string,
            input_html: {
              data: {
                provide: 'datepicker', date_format: 'yyyy-mm-dd'
              }
            }%>
    </div>
    <%= render partial: 'shared/edit_new/save_button', locals: {model_name: find_model_name_for_view(@license)} %>
  </form>
<% end %>

app/assets/javascripts/application.js

//= require jquery
//= require jquery.turbolinks
//= require jquery_ujs
//= require bootstrap-sprockets
//= require cocoon
//= require turbolinks
//= require data-confirm-modal
//= require jquery.geocomplete
//= require bootstrap-datepicker/core
//= require underscore
//= require gmaps/google
//
//= require init
//= require licenses/app.form.js

app/assets/javascripts/init.js

window.App || (window.App = {});

// Define init function on the App object
App.init = function(){

  // Bootstrap tooltips. Must be manually enabled.
  $(function () {
    $('[data-tooltip="true"]').tooltip();
  })
};

// Call the init function on every page transition.
// $(document).ready functions don't fire with turbolinks
$(document).on("turbolinks:load", function() {
  return App.init();
});

app/assets/javascripts/licenses/app.form.js

App.license = (function() {
  function licensesFormBehaviour() {

    //Update address select based on Company choice
    $('select#license_company_id').change(function() {
      return $.ajax({
        url: 'addresses',
        type: 'GET',
        dataType: 'script',
        data: {
          company_id: $("select#license_company_id option:selected").val()
        },
        error: function(jqXHR, textStatus, errorThrown) {
          return console.log("AJAX Error: " + textStatus);
        }
      });
    });

  // Several other unrelated methods, which function fine.
  // I've omitted them to make the code block shorter.

  return licensesFormBehaviour;
})();

$(document).on("ajaxSuccess", function() {
  //Licenses form only
  if (!($("form.edit_license").length > 0) && !($("form.new_license").length > 0) ) {
    return;
  }
  //invoke
  var license = new App.license();
  return license;
});

Thanks for you time, Patrick.

Edit:

Response, requested by fanta.

AJAX response

// Within the append() I've omitted around 30 additional <option> tags, for readability. 
$("#license_address_id").empty().append("<option value=\"\"></option><option value=\"197\">Berge Knolls<\/option>");

Solution

  • You mentioned that you load in the form via ajax, and hence initialise the form on ajaxSuccess, but as fanta mentioned in the comments, licences are also being initialised after the select request is complete, resulting in multiple instances.

    To solve this, you could store the licence instance in a data property, and only initialise if it does not already exist on the form element:

    $(document).on("ajaxSuccess", function() {
      $("form.edit_license, form.new_license").each(function(i, form) {
        var $form = $(form);
        var license = $form.data('license');
        if (!license) $form.data('license', new App.license());
      })
    });
    

    Bonus stuff: Rails (or rather jquery-ujs) already gives you the select/AJAX behaviour for free! :)

    If you add data-remote, data-url, and data-method to your select element, jquery-ujs will serialise the value of the element make the request for you. In your case, with simple_form, this is a little trickier, but if the documentation is correct, then you may be able to do something like:

    <%= f.input :company do %>
      <%= f.select
        :company,
        options_from_collection_for_select(
          Company.sorted_by('company_name_asc'), 'id', 'company_name'
        ),
        { include_blank: false },
        data: {
          remote: true,
          method: 'get',
          url: addresses_path
        }
      %>
    <% end %>
    

    When the select changes, jquery-ujs, will GET to /addresses?company_id=:selected_id, where your response can take over.

    Hope that helps!