Search code examples
ruby-on-railsjquery-select2ruby-on-rails-7import-maps

Trying to use select2 with Importmaps on Rails 7


I am trying to use Select2 on a new Rails 7 app and am struggling as follows:

I have pinned it into my import maps and imported it like so:

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "trix"
pin "@rails/actiontext", to: "actiontext.js"
pin "select2", to: "https://ga.jspm.io/npm:select2@4.1.0-rc.0/dist/js/select2.js"
pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js"

(the two last lines were added when I run bin/importmap pin select2)

import "jquery";
import "select2";
import "@hotwired/turbo-rails";
import "controllers";
import "trix";
import "@rails/actiontext";

(have moved both jquery and select2 to the end as well as to the beginning - didn't change a thing).

When I am in a form, I can access an element with $ like so:

$('#book_genre_ids');
...(returns the element)

But when I manually try - in the console - to run select2() on an element, here's what happens:

$('#book_genre_ids').select2();
VM574:1 Uncaught TypeError: $(...).select2 is not a function
    at <anonymous>:1:22

I did check the network sources (chrome console), and I could find npm:jquery@3.6.0/dist and npm:select2@4.1.0-rc.0/dist from gap.jspm.io. I found some resources that pointed at multiple jquery libraries being loaded, but I didn't find more than the above in the network sources in the console...


Solution

  • Select2 will register itself as a jQuery function .select2(), so in order to using this method, the select2 lib must be loaded after the jquery lib, otherwise it could not find jquery reference, hence it could not register jquery function, hence the error $(...).select2 is not a function will be throw.

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js" />
    

    However, the import is asynchronous loading so it's not guarantee that when select2 look for jquery, this lib is loaded. So although you setup import them in order as below:

    import "jquery";
    import "select2";
    

    The select2 is still not found the jquery when it needed.

    Fortunately, the gem importmap-rails support preloading pin modules (modulepreload), so base on that i come up with a solution: preload jquery before select2

    # config/importmap.rb
    pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js", preload: true
    pin "select2", to: "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"
    

    Then i follow the way Rails7 setup "@hotwired/stimulus" to setup "jquery"

    // app/javascript/controllers/application.js
    import { Application } from "@hotwired/stimulus"
    ...
    import jQuery from "jquery"
    window.jQuery = jQuery // <- "select2" will check this
    window.$ = jQuery
    ...
    

    now on stimulus controllers where "select2" is needed, you could load "select2"

    // app/javascript/controllers/demo_controller.js
    import { Controller } from "@hotwired/stimulus"
    import "select2"
    
    export default class extends Controller {
      initialize() {
        $('.js-example-basic-multiple').select2();
      }
    // ...
    

    Note: your "select2" CDN "https://ga.jspm.io/npm:select2@4.1.0-rc.0/dist/js/select2.js" source contain import e from"jquery"; at the first line hence it'll not work in this solution, so i recommend using the official cdn link: "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js" instead.

    update

    In case you don't want to use stimulus, you could pin another js file to setup "select2" and load it in your layout views

    // app/javascript/utils.js
    import "select2"
    
    $(document).ready(function () {
      $('.js-example-basic-multiple').select2();
    });
    
    # config/importmap.rb
    ...
    pin "utils"
    
    # app/views/products/show.html.erb
    <%= javascript_import_module_tag("utils") %>
    ...
    

    Note: you have to use javascript_import_module_tag, not javascript_importmap_tags, reference