Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails - Synchronise button clicks among tabs

I have a view with bootstrap-tabs. The tabs are generated dynamically.

<%= form_with(:id => 'my-form', model: [:admin, @island], local: true) do |form| %>
  <div class="tab-content bg-light" id="tabs-from-locales-content">

    <%= available_locales.each_with_index do |locale, i| %>
      <div
      class="tab-pane fade <%= 'show active' if i == 0 %>"
      id="<%= locale.downcase %>"
      role="tabpanel"
      aria-labelledby="<%= locale.downcase %>-tab">
        <%= render partial: 'island_form', locals: {counter: i, locale: locale, f: form} %>
      </div>
    <% end %>

  </div>

...
...

A tab represents each available localization of the app. The model of the form contains two nested attributes. These attributes have 1 to many relationship with the model. So the user can add multiple of these from the form. Their fields can be generated dynamically:

(For simplicity I include in the question only one. This is a part of _island_form.html.erb partial.)

<div class="form-group ports-div">
  <%= f.label :port %> </br>
  <%= f.fields_for :ports do |builder| %>

    <%= render 'port_fields', f: builder %>

  <% end %>
  <%= link_to_add_fields t('form.add_port'), f, :ports %>
</div>

<div class="form-group">
  <%= f.label :airport %> </br>
  <%= f.fields_for :airports do |builder| %>

    <%= render 'airport_fields', f: builder %>

  <% end %>
  <%= link_to_add_fields t('form.add_airport'), f, :airports %>
</div>

And the port_fields partial:

<fieldset>
  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>
  <%= f.hidden_field :_destroy %>
  <%= link_to t('form.remove'), '#', class: 'remove_fields' %>
</fieldset>

The link_to_add_fields helper method:

  def link_to_add_fields(name, f, association)
    # Builds an instance of the association record.
    new_object = f.object.send(association).klass.new
    # Grabbing ruby's object id.
    id = new_object.object_id
    fields = f.fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize + '_fields', f: builder)
    end
    link_to(name, '#', class: 'add_fields', data: { id: id, fields: fields.gsub('\n', '') })
  end

What I want to achieve is to synchronize the addition and removal of these fields among the available tabs. So when the user clicks Add field on the first tab all the other tabs will follow this action. Same for field removal.

My relevant js file for the Add button looks like this. I have tried many combinations between the trigger, triggerHandler and stopPropagation, although most of the times I am getting StackOverflow exception and the fields are added only to the tab that I clicked the add button.

(Since I pass a class (.add_fields) in the selector isn't that supposed to be attached to all the elements with class .add_fields?)

form.on('click', '.add_fields', function (event, param) {
    event.stopPropagation();
    event.preventDefault();

    var time = new Date().getTime();
    var regexp = new RegExp($(this).data('id'), 'g');
    $(this).before($(this).data('fields').replace(regexp, time));

    $('.tab-pane').each(function (index) {

        $('.add_fields').trigger("click", ["custom_click"]);
        console.log('Tab - ' + index);
    })
});

EDIT

I am getting somewhere with that code:

    $('.ports-div').each(function (index) {

        $(this).find('.add_fields').each(function () {
            this.click();
        })
    });

Although, at all the tabs, instead of 1 field 6 fields are added. I added the ports-div to the div that wraps all the related elements.

like image 811
Manos Avatar asked Feb 03 '18 20:02

Manos


1 Answers

Complete rewrite...

The problem as you have seen is to be that your click event handler is triggering a click on all tab-panes, including the one you are handling currently. You are also not just clicking the a single item in the loop, but all that match '.add_fields'. This then leads to the click being handled again, recursively.

To prevent this, I suggest calling a function to add your fields directly, rather than triggering a click. If triggering a click is just easiest in your situation, consider the following example that does roughly what you want without the recursive error.

https://jsfiddle.net/3ftf0j8e/1/

Dummy HTML

<a class="add_fields" id='1'>link 1</a>    
<a class="add_fields" id='2'>link 2</a>    
<a class="add_fields" id='3'>link 3</a>    
<a class="add_fields" id='4'>link 4</a>

Sample Javascript

$(document).on('click', '.add_fields', function (event, param) {
    event.preventDefault();


    var clicked_id = $(this).attr('id');
    console.log('clicked id:' + clicked_id);

    $(this).addClass('done');
    $(this).addClass('clicked');
    // Just click items that have not been clicked
    var els = $('.add_fields').not('.clicked');
    console.log(els);    
    els.trigger("click");

    setTimeout(function(){
      if($('.add_fields').length == $('.add_fields.done').length)
        $('.add_fields').removeClass('clicked');
    })

});

CSS

a.clicked {
  background-color: yellow;
}

a.done {
  color: red;
}

As you can see now, each fires just once. The setTimeout at the end allows the DOM to update before clearing the clicked classes.

like image 109
Phil Avatar answered Oct 10 '22 03:10

Phil