'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, er...ANYWHERE for that matter.

Sure importmaps is easy to manipulate as far as installing and mapping packages. Bravo. After that the whole documentation falls apart and a general haze clouds this new way of using js packages. Look around, there is plenty of confusion.

That said, how can I add this or something similar:

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

to application.js or wherever to get a those global functions to work, like $. 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

Ok. 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"
  }
}

Ding. Appears it is there. Then a simple script tag in application.html.erb:

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

Fail. Inspector says:

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

This really seems so very basic and is thoroughly irritating not to have documentation on some of the most used libraries with importmaps like jQuery and Bootstrap.

This is day 1 stuff, and yet docs are so very sparse on these things. Seems like there should be some coordinated effort to really explain a change away from webpack which in and of itself was another fiasco.

Please, if you know the answer to this, post it so that everyone can benefit.

Thanks.



Solution 1:[1]

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.

  • For firefox without importmap support the es-module-shim is used which can load the importmap after 'load' + 'DomContentLoaded' events.
  • For Chrome with importmap support the importmap is loaded before 'load' and 'DomContentLoaded'.
  • Additionally DOM might be loaded twice for various reasons.

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>
  • Alternatively inline the respective script directly, either by using the asset pipeline or straight the public folder. This is not recommended for two reasons: Inline scripts will be loaded aside importmap scripts. It is likely that inlining one script will lead to inline all its dependencies. (importmap scripts are not loaded here yet).

Example:

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

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 Mike Morgan