How to add custom JS file to new rails 7 project

Viewed 7752

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 "confing/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"
3 Answers

After watching DHH video I found the last piece of the puzzle.

To make my custom JS code work, I just added this line to the "confing/importmap.rb"

pin_all_from "app/javascript/custom", under: "custom"

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:

  1. Go to config/importmap.rb and add the following: pin_all_from "app/javascript/custom", under: "custom"
  2. Go to app/javascript/application.js file and add the following: import "custom/main"
  3. In 'app/javascript' directory, add 'custom' folder.
  4. In 'app/javascript/custom' directory add your custom js file 'main.js'.
  5. Run In your terminal: rails assets:precompile
  6. Start your rails server. VoilĂ 

Let's say we've added a plugin directory:

app
- javascript
  - plugin
      app.js
      index.js

Everything described below works because app/javascript directory

  • is part of Rails.application.config.assets.paths
  • and app/assets/config/manifest.js
# TLDR

# NOTE: pin a single file
#       file name is relative to `app/javascript`; don't add `.js`
#
pin("plugin/app")        # import "plugin/app"
pin("plugin/index")      # import "plugin/index"


# NOTE: pin all the files in `plugin` directory and subdirectories.
#       directory name is relative to `Rails.root`
#
pin_all_from("app/javascript/plugin", under: "plugin")
                         # import "plugin/app"
                         # import "plugin" // will import plugin/index.js

It seems pin_all_from did not get any documentation besides that it's there in the default importmap.rb.

pin_all_from(dir, under: nil, to: nil, preload: false)

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 pin prefix. Required if you have index.js file.

:to - Optional 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">

We can 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:

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

To pin a single file use pin method.

pin(name, to: nil, preload: false)

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.

pin("plugin/app")
pin("plugin/index")

# NOTE: produces these pins 
{
  "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"
  }
} 

Don't forget that pinning your files doesn't make them load. They have to imported in application.js or in a separate module tag:

<%= javascript_import_module_tag "plugin" %>

You can mess around with Importmap in the console, it's faster to debug and learn what works and what doesn't:

>> map = Importmap::Map.new
>> map.pin_all_from("app/javascript/plugin", under: "plugin")

# NOTE: ApplicationController has the required `path_to_asset` helper
>> ApplicationController.helpers.path_to_asset("plugin/app.js")
=> "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"

# NOTE: `path_to_asset` searches relative to these paths
>> Rails.application.config.assets.paths

>> puts map.to_json(resolver: ApplicationController.helpers)
{
  "imports": {
    "plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
    "plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
  }
}

# `pin` seems so much simpler now
>> map.pin("application")
>> puts map.to_json(resolver: ApplicationController.helpers)
{
  "imports": {
    "application": "/assets/application-8cab2d9024ef6f21fd55792af40001fd4ee1b72b8b7e14743452fab1348b4f5a.js"
  }
}

# Importmap from config/importmap.rb
>> Rails.application.importmap
Related