目次
1.はじめに
2.バニラJSでの実装
3.Stimulusでの実装
4.バニラJSとStimulusの比較
5.まとめ
1. はじめに
本記事は、Rails8でのJavaScript連携方法をまとめた記事となっております。
HTMLとJavaScriptの連携方法を フレームワークを使わない素のJS(バニラJS) と Stimulusの両方で実装して比較します。
学習ログを兼ねつつ、これからRailsでJSを書く人の参考になることを目的としています。
2. バニラJSでの実装
2-1. ファイルの置き場所
自作のJavaScriptファイルを app/javascript/
に置きます。
例:app/javascript/hamburger.js
//app/javascript/hamburger.js
document.addEventListener("turbo:load", () => {
const btn = document.getElementById("hamburger-btn");
const nav = document.getElementById("header__nav");
if (!btn || !nav) return;
btn.addEventListener("click", () => {
const expanded = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", String(!expanded));
btn.classList.toggle("is-active");
nav.classList.toggle("is-open");
});
nav.addEventListener("click", () => {
nav.classList.remove("is-open");
btn.classList.remove("is-active");
btn.setAttribute("aria-expanded", "false");
});
});
2-2. レイアウトでJSを読み込む
app/views/layouts/application.html.erb
の head
内に以下を記述します。
<head>
<%= javascript_importmap_tags %>
</head>
Rails 7 以降は Importmap を通してJSを読み込む仕組みになっています。
2-3. importmap の設定
config/importmap.rb
にファイルを登録します。
# config/importmap.rb
pin "hamburger", to: "hamburger.js"
Importmapとは、ブラウザに対して「どのモジュール名がどのファイルに対応しているか」を教える仕組みです。
この1行で、ブラウザは import "hamburger"
と書かれたときに、app/javascript/hamburger.js
を読み込むようになります。
2-4. エントリで読み込む
最後に app/javascript/application.js
で読み込みます。
// app/javascript/application.js
import "hamburger"
app/javascript/application.js
は、Rails アプリにおける JavaScript のエントリーポイントです。
このように書くと先ほど importmap.rb
で設定した "hamburger" → "hamburger.js"
の対応が適応され、app/javascript/hamburger.js
が読み込まれます。
これで、Railsアプリ内で hamburger.js が動作するようになります。
3. Stimulusでの実装
3-0. Stimulusとは?
Stimulusは Rails公式で推奨される軽量JSフレームワークです。
特徴は以下の通り:
- HTMLの
data-*
属性と連携する仕組み - コントローラ単位でコードを分割できる
- Turboと相性が良く、ページ遷移時も自動で再接続される
3-1. ファイルの置き場所
Stimulusを用いる場合、JavaScriptはapp/javascript/controllers/
にファイルを作成して記述します。
app/javascript/
├─application.js
└─controllers/
├─index.js
└─hamburger_controller.js ←作成したファイル
例:app/javascript/controllers/hamburger_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["button", "nav"]
toggle() {
this.buttonTarget.classList.toggle("is-active")
this.navTarget.classList.toggle("is-open")
}
close() {
this.buttonTarget.classList.remove("is-active")
this.navTarget.classList.remove("is-open")
}
}
3-2. レイアウトでJSを読み込む
2-2と同様に、app/views/layouts/application.html.erb
の head
内に以下を記述します。
<head>
<%= javascript_importmap_tags %>
</head>
3-3. importmap の設定
config/importmap.rb
にファイルを登録します。
# config/importmap.rb
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/controllers", under: "controllers"
のように書くことで、app/javascript/controllers
下のファイルを自動的に紐づけてくれます。
例えば、app/javascript/controllers/hamburger_controller.js
というファイルがある場合、以下と同じ意味になります。
# config/importmap.rb
pin "hamburger_controller", to: "hamburger_controller.js"
3-4. エントリ(application.js)と登録方法
ここまでで config/importmap.rb
に Stimulus と pin_all_from
を設定し、レイアウトに <%= javascript_importmap_tags %>
を入れました。
最後にエントリ(app/javascript/application.js
)で Stimulus を起動し、コントローラを登録します。
import { Application } from "@hotwired/stimulus"
const application = Application.start()
application.debug = false
window.Stimulus = application
export { application }
4. バニラJSとStimulusの比較
4-1. バニラJSとHTMLの連携
バニラJSでは、DOM探索をgetElementById
などを用いて行う必要があります。ハンバーガーボタンの実装を例に解説します。
JavaScript側(hamburger.js
)
document.addEventListener("turbo:load", () => {
const btn = document.getElementById("hamburger-btn");
const nav = document.getElementById("header__nav");
if (!btn || !nav) return;
btn.addEventListener("click", () => {
const expanded = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", String(!expanded));
btn.classList.toggle("is-active");
nav.classList.toggle("is-open");
});
nav.addEventListener("click", () => {
nav.classList.remove("is-open");
btn.classList.remove("is-active");
btn.setAttribute("aria-expanded", "false");
});
});
HTML側(application.html.erb
)
<!-- ハンバーガーボタン -->
<button
id="hamburger-btn"
class="hamburger-btn"
aria-expanded="false"
aria-controls="header__nav"
>
<span class="hamburger-bar"></span>
<span class="hamburger-bar"></span>
<span class="hamburger-bar"></span>
</button>
このようにバニラJSでは、button
タグにidを設定した上でbtn.getAttribute("")
のように受け取る必要があります。
4-2. StimulusとHTMLの連携
Stimulusでは、JavaScriptとHTMLの連携を data-controller と data-action を使って行います。
これにより、DOM探索を自分で書かずに、HTMLの属性として紐づけるだけでイベントを処理できます。
JavaScript側(hamburger_controller.js
)
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["button", "nav"]
toggle() {
this.buttonTarget.classList.toggle("is-active")
this.navTarget.classList.toggle("is-open")
}
close() {
this.buttonTarget.classList.remove("is-active")
this.navTarget.classList.remove("is-open")
}
}
HTML側(application.html.erb
)
<!-- ハンバーガーボタン -->
<div data-controller="hamburger">
<button
data-hamburger-target="button"
data-action="click->hamburger#toggle"
class="hamburger-btn"
aria-expanded="false"
aria-controls="header__nav"
>
<!-- ボタン表示 -->
</button>
<nav id="header__nav" data-hamburger-target="nav">
<!-- ナビゲーション内容 -->
</nav>
</div>
Stimulusでは data-* 属性を付与するだけで要素とコードが結びつきます。
4-3. バニラJSとStimulusを使ってみた感想
バニラJSは、getElementById や querySelector を使ってDOMを取得する方法が直感的で、初心者の私でも迷わず実装することができました。ただしイベントが増えていくと addEventListener が多くなり、コードが複雑に見えてしまう点が気になりました。
一方、Stimulusを使ったときは、data-controller や data-action といった仕組みによって、HTMLと自然に結びつけられるのが便利でした。コントローラごとにコードが分かれているため、後から見返したときにも「どの部分が何を担当しているのか」がすぐに分かります。
全体を通してみると、ちょっとした動きを追加する程度ならバニラJSで十分ですが、アプリケーションを育てていくのであればStimulusを選んだ方が保守や拡張が楽になるというのが率直な感想です。Rails 7以降では公式にStimulusが推奨されていることもあり、迷ったときはStimulusを使うのが無難だと感じました。
まとめ
- Rails 8 では Importmap により JS を簡単に読み込める
- バニラJSは簡単な実装向きだが、複雑になるとコードが煩雑になりやすい
- Stimulusは公式推奨で、拡張性・保守性に優れる
- 迷ったら Stimulus を選ぶのが無難