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?
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 scripts are executed first. That's before the importmap JS scripts are loaded. See MDN script docs.
Importmap is still very new and support varies. This complicates things.
es-module-shim
is used which can load the
importmap after 'load' + 'DomContentLoaded' events.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
.
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 inapplication.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:
importmap.rb
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;
.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>
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>