Reputation: 861
I created new rails 7 project rails new my_project
and have a problem to include my custom JS file to be processed by rails.
my "javascript/application.js"
import "@hotwired/turbo-rails"
import "controllers"
import "chartkick"
import "Chart.bundle"
import "custom/uni_toggle"
my custom JS file: "javascript/custom/uni_toggle.js"
function uniToggleShow() {
document.querySelectorAll(".uni-toggle").forEach(e => e.classList.remove("hidden"))
}
function uniToggleHide() {
console.log("uni toggle hide")
document.querySelectorAll(".uni-toggle").forEach(e => e.classList.add("hidden"))
}
window.uniToggleShow = uniToggleShow
window.uniToggleHide = uniToggleHide
I'm using in my layout <%= javascript_importmap_tags %>
and my "config/importmap.rb"
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"
Upvotes: 50
Views: 38117
Reputation: 29719
1. Quickstart - quick things you should know
2. `pin_all_from` - a few details
3. `pin` ...
4. Run in a console - when you need to figure stuff out
5. Relative imports - don't do it, unless you want to
6. Examples - to make it extra clear
If you're not using importmap-rails
, really, you should not have any issues. Add a file then import "./path/to/file"
. Make sure to run bin/dev
to compile your javascript if you're using jsbundling-rails
.
If you're using importmap-rails
, there is no compilation, every single file has to be served individually in development and production, and every import has to be mapped to a url for browser to fetch.
pin
and pin_all_from
is a rails way of constructing an importmap
. Imports are mapped to local files through an asset url. So just keep in mind, import "something"
could map to url /assets/file-123.js
which could map to file app/some_asset_path/file.js
or in production public/assets/file-123.js
:
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-da9b182f12cdd2de0b86de67fc7fde8d1887a7d9ffbde46937f8cd8553cb748d.js",
"@hotwired/turbo-rails": "/assets/turbo.min-49f8a244b039107fa6d058adce740847d31bdf3832c043b860ebcda099c0688c.js",
"@hotwired/stimulus": "/assets/stimulus-a1299f07b3a1d1083084767c6e16a178a910487c81874b80623f7f2e48f99a86.js",
"@hotwired/stimulus-loading": "/assets/stimulus-loading-6024ee603e0509bba59098881b54a52936debca30ff797835b5ec6a4ef77ba37.js",
"controllers/application": "/assets/controllers/application-44e5edd38372876617b8ba873a82d48737d4c089e5180f706bdea0bb7b6370be.js",
"controllers/hello_controller": "/assets/controllers/hello_controller-29468750494634340c5c12678fe2cdc3bee371e74ac4e9de625cdb7a89faf11b.js",
"controllers": "/assets/controllers/index-e70bed6fafbd4e4aae72f8c6fce4381d19507272ff2ff0febb3f775447accb4b.js",
}# ^ ^
# | |
# names you use to import urls browser uses to get it
# | ^
# | |
# `------> mapped to ---------'
}</script>
Once you have an importmap you have to import
the things you need. Importmap doesn't load anything, it is just a configuration.
Let's say we've added a plugin directory:
app/
└── javascript/
├── application.js # <= imports go here and other js files
└── plugin/
├── app.js
└── index.js
config/
└── importmap.rb # <= pins go here
Pin a single file:
# config/importmap.rb
pin "plugin/app"
pin "plugin/index"
# app/javascript/application.js
import "plugin/app" # which maps to a url which maps to a file
import "plugin/index"
or pin all the files in plugin directory and subdirectories:
# config/importmap.rb
pin_all_from "app/javascript/plugin", under: "plugin"
# app/javascript/application.js
import "plugin/app"
import "plugin" # will import plugin/index.js
Do not use relative imports, such as import "./plugin/app"
, it may work in development, but it will break in production.
See the output of bin/importmap json
to know what you can import and verify importmap.rb config.
Do not precompile in development, it will serve precompiled assets from public/assets
which do not update when you make changes.
Run bin/rails assets:clobber
to remove precompiled assets.
In case something doesn't work, app/javascript directory has to be in:
Rails.application.config.assets.paths
and app/assets/config/manifest.js
as //= link_tree ../../javascript .js
Pinning your files doesn't make them load. They have to be imported in application.js
:
// app/javascript/application.js
import "plugin"
Alternatively, if you want to split up your bundle, you can use a separate module tag in your layout:
<%= javascript_import_module_tag "plugin" %>
or templates:
<% content_for :head do %>
<%= javascript_import_module_tag "plugin" %>
<% end %>
# add this to the end of the <head> tag:
# <%= yield :head %>
You can also add another entrypoint in addition to application.js
, say you've added app/javascript/admin.js
. You can import it with all the pins:
# this doesn't `import` application.js anymore
<%= javascript_importmap_tags "admin" %>
Because application
pin has preload: true
option set by default it will issue a request to load application.js
file, even when you override application
entrypoint with admin
. Preloading and importing are two separate things, one does not cause the other. Remove preload
option to avoid unnecessary request.
Pins all the files in a directory and subdirectories.
https://github.com/rails/importmap-rails/blob/v1.1.2/lib/importmap/map.rb#L33
def pin_all_from(dir, under: nil, to: nil, preload: false)
clear_cache
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload)
end
dir
- Path relative to Rails.root or an absolute path.
Options:
:under
- Optional[1] pin prefix. Required if you have index.js
file.
:to
- Optional[1] path to asset. Falls back to :under option. Required if :under is omitted.
This path is relative to Rails.application.config.assets.paths.
:preload
- Adds a modulepreload link if set to true
:
<link rel="modulepreload" href="/assets/turbo-5605bff731621f9ca32b71f5270be7faa9ccb0c7c810187880b97e74175d85e2.js">
:under
or :to
is requiredTo pin all the files in the plugin directory:
pin_all_from "app/javascript/plugin", under: "plugin"
# NOTE: `index.js` file gets a special treatment, instead
# of pinning `plugin/index` it is just `plugin`.
{
"imports": {
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
Here is how it all fits together:
(if something doesn't work, take your options and follow the arrows, especially the path_to_asset
part, you can try it in the console, see below)
"plugin/app": "/assets/plugin/app-04024382391bb...4145d8113cf788597.js"
# ^ ^ ^
# | | |
# :under | `-path_to_asset("plugin/app.js")
# | ^ ^
# | | |
# |.. (:to||:under)-' |
# "#{dir}/app.js" |
# '''''`-------------------------'
:to
option might not be obvious here. It is useful if :under option is changed, which will make path_to_asset
fail to find app.js.
For example, :under option can be anything you want, but :to option has to be a path that asset pipeline, Sprockets, can find (see Rails.application.config.assets.paths) and also precompile (see app/assets/config/manifest.js).
pin_all_from "app/javascript/plugin", under: "@plug", to: "plugin"
# Outputs these pins
#
# "@plug/app": "/assets/plugin/app-04024382391b1...16beb14ce788597.js"
# "@plug": "/assets/plugin/index-04024382391bb91...4ebeb14ce788597.js"
#
# and can be used like this
#
# import "@plug";
# import "@plug/app";
Specifying absolute path will bypass asset pipeline:
pin_all_from("app/javascript/plugin", under: "plugin", to: "/plugin")
# "plugin/app": "/plugin/app.js"
# "plugin": "/plugin/index.js"
#
# NOTE: It is up to you to set up `/plugin/*` route and serve these files.
Pins a single file.
https://github.com/rails/importmap-rails/blob/v1.1.2/lib/importmap/map.rb#L28
def pin(name, to: nil, preload: false)
clear_cache
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
end
name
- Name of the pin.
Options:
:to
- Optional path to asset. Falls back to {name}.js
. This path is relative to Rails.application.config.assets.paths.
:preload
- Adds a modulepreload link if set to true
When pinning a local file, specify name relative to app/javascript directory (or vendor or any other asset directory).
pin "plugin/app"
pin "plugin/index"
{
"imports": {
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin/index": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
Here is how it fits together:
"plugin/app": "/assets/plugin/app-04024382391bb...16cebeb14ce788597.js"
# ^ ^
# | |
# name `-path_to_asset("plugin/app.js")
# ^
# |
# (:to||"#{name}.js")-'
If you want to change the name of the pin, :to
option is required to give path_to_asset a valid file location.
For example, to get the same pin for index.js file as the one we get from pin_all_from:
pin "plugin", to: "plugin/index"
{
"imports": {
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
You can mess around with Importmap
in the console, it's faster to debug and learn what works and what doesn't:
>> helper.path_to_asset("plugin/app.js")
=> "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
>> map = Importmap::Map.new
>> map.pin_all_from("app/javascript/plugin", under: "plugin")
>> puts map.to_json(resolver: helper)
{
"imports": {
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
>> map.pin("application")
>> puts map.to_json(resolver: helper)
{
"imports": {
"application": "/assets/application-8cab2d9024ef6f21fd55792af40001fd4ee1b72b8b7e14743452fab1348b4f5a.js"
}
}
# Importmap from config/importmap.rb
>> Rails.application.importmap
Relative imports could work, if you make the correct mapping:
# config/importmap.rb
pin "/assets/plugin/app", to: "plugin/app.js"
// app/javascript/application.js
import "./plugin/app"
application.js is mapped to digested /assets/application-123.js, because ./plugin/app
is relative to /assets/application-123.js, it should be correctly resolved to /assets/plugin/app
which has an importmap that we made with our pin:
"/assets/plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
This should also just work:
// app/javascript/plugin/index.js
import "./app"
However, while import-maps
support all the relative and absolute imports, this doesn't seem to be the intended use case in importmap-rails
.
This should cover just about everything:
.
├── app/
│ └── javascript/
│ ├── admin.js
│ ├── application.js
│ ├── extra/
│ │ └── nested/
│ │ └── directory/
│ │ └── special.js
│ └── plugin/
│ ├── app.js
│ └── index.js
└── vendor/
└── javascript/
├── downloaded.js
└── package/
└── vendored.js
Output is from running bin/importmap json
:
# this is the only time when both `to` and `under` options can be omitted
# you don't really want to do this, at least not for `app/javascript`
pin_all_from "app/javascript"
pin_all_from "vendor/javascript"
"admin": "/assets/admin-761ee3050e9046942e5918c64dbfee795eeade86bf3fec34ec126c0d43c931b0.js",
"application": "/assets/application-d0d262731ff4f756b418662f3149e17b608d2aab7898bb983abeb669cc73bf2e.js",
"extra/nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"downloaded": "/assets/downloaded-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"package/vendored": "/assets/package/vendored-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
Note the difference:
pin_all_from "app/javascript/extra", under: "extra" # `to: "extra"` is implied
"extra/nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
^
pin_all_from "app/javascript/extra", to: "extra"
"nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
^
pin_all_from "app/javascript/extra", under: "@name", to: "extra"
"@name/nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
^
Note the pattern:
pin_all_from "app/javascript"
pin_all_from "app/javascript/extra", under: "extra"
pin_all_from "app/javascript/extra/nested", under: "extra/nested"
pin_all_from "app/javascript/extra/nested/directory", under: "extra/nested/directory"
pin_all_from "app/javascript/extra", to: "extra"
pin_all_from "app/javascript/extra/nested", to: "extra/nested"
pin_all_from "app/javascript/extra/nested/directory", to: "extra/nested/directory"
pin_all_from "app/javascript/extra", under: "@name", to: "extra"
pin_all_from "app/javascript/extra/nested", under: "@name", to: "extra/nested"
pin_all_from "app/javascript/extra/nested/directory", under: "@name", to: "extra/nested/directory"
Same exact thing works for vendor:
pin_all_from "vendor/javascript/package", under: "package"
# etc
Single files are easy:
pin "admin"
pin "application"
pin "extra/nested/directory/special"
pin "@extra/special", to: "extra/nested/directory/special"
pin "downloaded"
pin "renamed", to: "downloaded"
Upvotes: 132
Reputation: 557
Was also having trouble adding custom JS files in my Rails 7 app. I even followed DHH video --> https://www.youtube.com/watch?v=PtxZvFnL2i0 but still was facing difficulties. The following steps worked for me:
Go to config/importmap.rb and add the following:
pin_all_from "app/javascript/custom", under: "custom"
Go to app/javascript/application.js file and add the following:
import "custom/main"
In 'app/javascript' directory, add 'custom' folder.
In 'app/javascript/custom' directory add your custom js file 'main.js'.
Run In your terminal:
rails assets:precompile
Start your rails server. Voilà 👍
Upvotes: 27
Reputation: 91
If you want to use importmap, do what people have answer before.
But if you add the file in the importmap it means that in every layout, the file will be loaded, but if you only want to add single JS file, my suggestion is to use a simple , with javascript_include_tag like this:
<%= javascript_include_tag 'filename' %>
Upvotes: 2