jqueryruby-on-railsruby-on-rails-7import-maps

Rails 7 (7.0.2.3) Importmap jQuery is not defined in view


I've looked around for as much help as possible regarding installing jQuery in Rails 7 (7.0.2.3). I want to use it in script tags in my views, but I can't seem to get it exported to where it is globally available or anywhere for that matter.

importmaps is easy to manipulate as far as installing and mapping packages, however after that the documentation is unclear.

I am trying to figure out how to add something similar to this:

import jquery from "jquery"
window.jQuery = jquery;
window.$ = jquery;

to application.js to get global functions, like $, to work, as I'd like $ to be available in all my views.

As for what I've done:

./bin/importmap pin jquery --download

Gives me the importmap line:

pin "jquery" # @3.6.0

Then looking at the importmap JSON:

{
  "imports": {
    "application": "/assets/application-37a24e4747cc3cde854cbbd628efbdf8f909f7b031a9ec5d22c5052b06207eb8.js",
    "@hotwired/turbo-rails": "/assets/turbo.min-96cbf52c71021ba210235aaeec4720012d2c1df7d2dab3770cfa49eea3bb09da.js",
    "@hotwired/stimulus": "/assets/stimulus.min-900648768bd96f3faeba359cf33c1bd01ca424ca4d2d05f36a5d8345112ae93c.js",
    "@hotwired/stimulus-loading": "/assets/stimulus-loading-1fc59770fb1654500044afd3f5f6d7d00800e5be36746d55b94a2963a7a228aa.js",
    "jquery": "/assets/jquery-498b35766beec7b412bab57a5acbe41761daa65aa7090857db4e973fa88a5623.js",
    "controllers/application": "/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js",
    "controllers/hello_controller": "/assets/controllers/hello_controller-549135e8e7c683a538c3d6d517339ba470fcfb79d62f738a0a089ba41851a554.js",
    "controllers": "/assets/controllers/index-7a8fc081f7e391bd7b6fba95a75e36f88ba813da2c4c8787adad248afb9a0a06.js"
  }
}

It appears jQuery is there but a simple script tag in application.html.erb:

<script type="text/javascript" charset="utf-8">
        $(document).ready(function (){
            console.log('jQuery working.');
        })
</script>

Fails with the error:

(index):41 Uncaught ReferenceError: $ is not defined

This really seems basic, yet docs are very sparse on these things.

Can someone please explain why this occurs and how to correct it?


Solution

  • Discussion

    As of now (Apr-2022) There are two things to consider: inline script loading and browser importmap support. Both together can make inlining scripts which refer to variables defined through importmap counter-intuivite and error-prone.

    Inline Script Loading

    Inline scripts are executed first. That's before the importmap JS scripts are loaded. See MDN script docs.

    Browser Importmap Support

    Importmap is still very new and support varies. This complicates things.

    Conclusion

    The variable- and function- hoisting mechanism takes care of script loading sequence problems, but in this case because the script is defined inline and importmap scripts have not yet been loaded, the variable is undeclared and it will definitively result in a ReferenceError: ... is not defined.

    Solutions

    Ensure the variable is defined before accessing, by checking if scripts from the importmap have been loaded within the inline script and before running code accessing them.

    Most reliable is placing a variable into application.js and checking for its declaration safely in the inline script. If it exists importmap has been loaded and everything in application.js exists in the inline script context.

    The document.DomContentLoaded event or the window.load event or both can be used for this in conjunction with the in keyword. Alternatively a custom event can be thrown at the end of application.js to enforce running the inline code only after the importmap code has been loaded.

    Example:

    1. After pinning jquery in importmap.rb
    2. In app/javascript/application.js
    // jquery does not export 'default' but defines window.$ and
    // window.jQuery when loaded:
    // - import 'jquery'; will not work
    // - namespace does not matter here (jq)
    // - no need to redefine it again w/ window.$ = jq.$
    import * as jq from 'jquery';
    
    // Define a variable to check in inlined HTML script
    window.importmapScriptsLoaded = true;
    
    1. In the .html / .erb.html

    NOTE: Depending on injected shims/polyfills the load order might still be undefined. In this case more work is needed (like throwing a custom event). Also the DOM might be loaded twice which needs to be taken into account when code shan't be double executed.

    <h1 id="hello">Hello</h1>
    
    <script type="text/javascript">
    
    // Guard against double DOM loads
    var codeExecuted = false;
    
    document.addEventListener('DOMContentLoaded', function(e) {
    
      // Check if importmap stuff exisits without throwing an error.
      // Then run main code w/ guard against multiple executions.
      if ("importmapScriptsLoaded" in window) { 
    
        if (!codeExecuted) {
          // Main code here
          console.log($('#hello'));
    
          // Don't forget to bump guard for one-time only JS execution !!
          codeExecuted = true; 
        };  
      };
    
    });
    </script>
    

    Example:

    <script type="text/javascript" src="/jquery.js"></script>