'Select2 with ajax gets initialized several times with Rails turbolinks events

I am a developing a Ruby On Rails app using Rails 4.2.6. I am using Turbolinks alongside jquery.turbolinks (sorry I could'nt post the links to those elements as I am a newbie on the site). My problem is very simple but I just can't solve it. Here it is: I have a form fetched through AJAX

<div class="card-footer">
  <a class="btn btn-sm btn-primary-outline" data-remote="true"  href="/profiles/Mke5kA/positions/new"><i class="fa fa-plus"></i> Nouvelle expérience professionnelle</a>
  <div id="new_position_form"></div>
</div>

The form contains Select2 elements that get their data through AJAX

= simple_form_for [profile, position], remote: true, html: {id: 'positionForm', class: 'm-b-1'} do |f|
  = f.input :company_id, as: :select, input_html: {:'data-behaviour' => 'company-select2', :'data-kind' => 'company'}
  = f.input :title
  = f.input :summary
  - location = f.object.build_location
  = f.simple_fields_for :location do |l|
    = render 'locations/fields', l: l, city: position.city
  = render "profiles/shared/date_fields", f: f, model: position
  = f.input :skill_list, as: :select, input_html: {multiple: true, :data => {:behaviour => 'acts-as-taggable', :'taggable-context' => 'skills'}}
  %button.btn.btn-primary{:type => "submit"}= icon('check-square-o', 'Enregistrer')
  = link_to icon('remove', 'Annuler'), 'javascript:void(0)', 
        data: {:'lgnk-behaviour' => "remove-form", :'lgnk-target' => "#positionForm" }, class: 'btn btn-secondary'
  • The Select2 elements are "activated" currently upon Rails Trubolinks events "page:load page:update", but I have also tried "page:change"
  • When the form is fetched: the select2 elements are fine (activated correctly):

Initial form (after AJAX fetch

My problem appears when I try typing in the Select2 that are using AJAX to get the data: all the select2s are duplicated:

enter image description here

Here is how I get the Select2 initialized:

var loc_tag = function() {
  $('[data-behaviour="acts-as-taggable"]').not('.select2-hidden-accessible').each (function (index, element) {
    if ($(element).data('value')) {
      var options = $(element).data('value').split(', ');
      $.each(options, function(key, tag){
        $(element).append($('<option selected></option>').val(tag).text(tag));
      });
    }

    $(element).select2({
      ajax: {
        url: "/tags?context="+$(element).data('taggable-context'),
        dataType: 'json',
        headers: {
         "Accept": "application/json"
        },
       delay: 250,
       data: function (params) {
         return {
           q: params.term, // search term
           page: params.page
         };
       },
       processResults: function (data, page) {
         return {
           results: data
         };
      },
      cache: true
    },
    escapeMarkup: function (markup) { return markup; }, // let our custom formatter work
    minimumInputLength: 2,
    tags: true,
    language: "fr",
    theme: "bootstrap",
    width: "100%",
    placeholder: 'Mots clés...'
    });
  });

};
$(document).on('page:load page:update', loc_tag);

I want the Select2 elements to get initialized only once (when the form is fetched) and not upon AJAX responses on them getting their data. I have tried jQuery.not(".select2-hiden-accessible") on the elements unsing Select2 (select2-hidden-accessible being the class Select2 adds to an initialized Select2 element) but it does not work.

Many thanks for your kind help!



Solution 1:[1]

When using Turbolinks 5 and select2, the select2 object is no longer attached (see below for test) to the <select> when using the back button to return to a page. A new select2 object is created and attached after going back but it was unusable.

jack's answer didn't work for me because when the new select2 object is added, the <select> still has class='select2-hidden-accessible' which, among other things, sets width: 1px !important. When the new select2 object is created it's basically invisible.

The key for me was to destroy all select2 objects before TL caches the page. Here is the solution that worked for me:

$(document).on("turbolinks:before-cache", function() {
  $('.select2-input').select2('destroy');
});

$(document).on('turbolinks:load', function() {
  $('.select2-input').select2();
});

More Detail

I believe this is the correct approach given the Turbolinks documentation (emphasis mine):

Preparing the Page to be Cached

Listen for the turbolinks:before-cache event if you need to prepare the document before Turbolinks caches it. You can use this event to reset forms, collapse expanded UI elements, or tear down any third-party widgets so the page is ready to be displayed again.

Testing select2 Existance

To test if the select2 object is attached to the <select> you can execute the following in the console:

('.select2-input').first().data('select2')

Solution 2:[2]

I have the same problem and I found that when you press the back button both the select and the select2 elements are rendered but they are not bound together so when you re-initialize it with $('select).select2() it creates another brand new select2 element next to it.

So here's what I did before initializing the select2:

If the select is not a select2 (i.e. $(el).data('select2') == undefined) but there is already a select2 element next to it, then remove it.

if ($(el).data('select2') == undefined && $(el).next().hasClass('select2-container')) {
  $(el).next().remove();
}
$(el).select2();

Solution 3:[3]

2021 UPDATE:

Using the turbolinks:before-cache:

document.addEventListener('turbolinks:before-cache', function () {
  // removing the select2 from all selects
  $("select").select2('destroy');
});

And loading the select2 when turbolinks:load:

document.addEventListener("turbolinks:load", function () {
  // applying select2 to all selects
  $("select").select2({
    theme: 'bootstrap4' // if you want to pass some custom config
  });
});

Of course, you can create a custom .select2-binder class or a data-select2-binder to choose which select will be affected.


I had the same issue and I solved by doing:

// init.js (you can pass an container instead of using document

$( document ).on('turbolinks:load', function() {
  $( document ).find('select').not('.select2-hidden-accessible').select2();
});

And now I don't have those duplicated selects :)

Solution 4:[4]

Nothing here worked for me.

I was able to get around this altogether by not caching the page with the select.

I use this on the top of the page with select: <% provide(:no_cache, true) %>

I use this on application.html.erb:

<% if yield(:no_cache) %>
  <meta name="turbolinks-cache-control" content="no-cache">
<% end %>

Solution 5:[5]

Note for rails 7 and select2.js users.

In rails 7 version turbo-links deprecated. New library's name is turbo. https://turbo.hotwired.dev/handbook/drive You can add the tag below to html's head section to prevent caching. So select2.js can work properly now. Documentation

<meta name="turbo-visit-control" content="reload">

Solution 6:[6]

It works for me. There is no duplication even though click on backward, forward or click other links and comeback to the page where the select2 was initialized.

document.addEventListener("turbolinks:load", function() {
  $('.select2-container').remove() // this will remove all the select2 containers
  $('.select2-input').select2(); // and this will reinit the select2 again
})

Update

I see some good advices here you can choose one of those. I chose to move all the javascript in the tag instead of put them in the body tag.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 snrlx
Solution 2 Jack
Solution 3
Solution 4 Sidney
Solution 5
Solution 6