Select2 with ajax gets initialized several times with Rails turbolinks events
Asked Answered
A

9

23

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!

Achaean answered 8/4, 2016 at 10:46 Comment(0)
R
43

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')
Rhythmist answered 28/1, 2017 at 21:39 Comment(4)
works for me!, thanks. And it's more elegant than Jack's oneTraver
You can destroy all select2 select with: document.addEventListener('turbolinks:before-cache', function() { $('.select2-hidden-accessible').select2('destroy'); });Gin
@Gin solution avoids an extra step of checking, works well.Whippoorwill
@Gin you solution works but need to add if($('.select2-input').first().data('select2')) condition to check that destroy only if select2 is attached to that inputDemy
T
6

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 :)

Timbered answered 26/8, 2016 at 18:16 Comment(1)
This indeed does not lead to duplicated selects, but when using the 'back' button to got back to a page with a select2 box the selectbox can no longer be selected. @jack's is a bit less elegant but the more proper solution here.Grau
R
6

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();
Reticular answered 23/11, 2016 at 15:18 Comment(1)
I feel like it really should be addressed upstream.Texas
K
2

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">
Kloof answered 6/4, 2022 at 9:32 Comment(0)
F
1

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 %>
Floro answered 18/2, 2018 at 23:47 Comment(0)
K
0

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.

Kra answered 2/9, 2020 at 16:43 Comment(0)
C
0

Rails 7 Update With Cache

Originally written here

Many things here won't work in Rails 7 especially turbolinks:before-cache event. New event that you are looking for is turbo:before-cache, and turbo:load so it would look like this:

$(document).on("turbo:before-cache", function() {
    $("#select_id").select2('destroy');
});

$(document).on('turbo:load', function() {
    $('#select_id').select2();
});
Citrus answered 8/1, 2024 at 14:54 Comment(0)
S
0

I've encountered a same situation with my Select2 where only the first dropdown is functional, while the duplicated dropdowns created during forward and backward browser navigation are not working. To address this issue, I've opted to remove all additional dropdowns, keeping only the first one. I've then utilized the 'popstate' event.

The popstate event of the Window interface is triggered when the active history entry changes during user navigation through the session history. This event adjusts the current history entry to match the last page the user visited, or, if applicable, the history entry.

This will remove the duplicated dropdown while keeping the original one.

$(window).on('popstate', function(event) {
   $('.select2-container:not(:first)').remove();
   });
Swage answered 25/1, 2024 at 12:11 Comment(0)
S
0

For rails 7 this working well

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    $(document).on("turbo:before-cache", function() {
      // To fix select2 doublication
      $(".selector").select2('destroy');
    });

    $(document).on('turbo:load', function() {
      // initialize multi-select
      $('.selector').select2();
    });
  }
}
Schizo answered 20/6, 2024 at 7:39 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.